Backend Architecture
The Qarion backend is built using FastAPI and is designed to be fully asynchronous, scalable, and modular.
Core Framework: FastAPI
We chose FastAPI for its performance (Starlette), standardization (Pydantic), and native async support.
Key Characteristics
- Async First: All I/O bound operations (Database, API calls) are awaited.
- Pydantic Validation: Request and response models are strictly typed and validated using Pydantic schemas.
- Dependency Injection: Heavy use of FastAPI's
Dependsfor providing services, database sessions, and current user context.
Service Layer Architecture
We follow a strict service layer pattern to decouple business logic from the API routes.
Pattern
- API Route (Controller): Handles HTTP request/response, validation, and invoking the service.
- Service Class: Contains the business logic. Instantiated per request with necessary dependencies (e.g.,
db_session,user_context). - Data Access (Repository/ORM): SQLAlchemy models are used directly within services for data access, but complex queries are often encapsulated in model methods or mixins.
Instance-Based Services
Services are designed as instance-based rather than static utilities. This allows them to hold state for the duration of a request (e.g., the current user, the active database session) and simplifies testing by allowing easy mocking of dependencies.
class ItemService:
def __init__(self, db: AsyncSession, user: User):
self.db = db
self.user = user
async def create_item(self, data: ItemCreate) -> Item:
# User is already available as self.user
item = Item(**data.dict(), owner_id=self.user.id)
self.db.add(item)
await self.db.commit()
return item
Asynchronous Architecture & Traps
Transitioning to a fully async backend introduces specific challenges ("hydration traps") which we have addressed with strict standards.
Standard #330-SL: Lazy='raise'
To prevent implicit synchronous I/O in async contexts (which block the loop), we configure all SQLAlchemy relationships with lazy='raise'.
- Problem: Accessing
item.ownerwhen strict loading wasn't requested triggers a sync SQL query. - Solution:
lazy='raise'causes an error if the relationship is accessed without being explicitly loaded viaoptions(selectinload(...)). - Benefit: Forces developers to be explicit about data fetching, preventing N+1 problems and ensuring async safety.
Background Tasks
Long-running operations are offloaded to background workers using Arq (Redis-based job queue).
- Quality Checks: Data quality validation runs asynchronously.
- Scrapers: Metadata ingestion happens in background tasks.
- Notifications: Sending emails or Slack alerts.
Timezone Handling (Standard #312-UTC)
All timestamps are stored in UTC in the database. Conversion to local time happens only at the presentation layer (frontend) or strictly for user-facing notifications.