Skip to main content

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 Depends for 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

  1. API Route (Controller): Handles HTTP request/response, validation, and invoking the service.
  2. Service Class: Contains the business logic. Instantiated per request with necessary dependencies (e.g., db_session, user_context).
  3. 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.owner when 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 via options(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.