Skip to content

Architecture Overview

CMS API follows a layered architecture where each layer has a single responsibility and communicates only with the layer directly below it.

Layers

┌─────────────────────────────────────┐
│              HTTP Request           │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│              Router                 │  app/features/{domain}/{module}/router.py
│  Receives request, returns response │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│              Schema                 │  app/features/{domain}/{module}/schema.py
│  Validates input, shapes output     │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│              Service                │  app/features/{domain}/{module}/service.py
│  Business logic and decisions       │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│            Repository               │  app/features/{domain}/{module}/repository.py
│  Database communication only        │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│              Model                  │  app/features/{domain}/{module}/model.py
│  SQLAlchemy table definitions       │
└──────────────────┬──────────────────┘
┌──────────────────▼──────────────────┐
│            PostgreSQL               │
└─────────────────────────────────────┘

Layer Responsibilities

Router

  • Receives HTTP requests
  • Invokes the appropriate service method
  • Returns HTTP responses with correct status codes
  • Has no business logic

Schema (Pydantic)

  • Validates and deserializes incoming request data
  • Serializes outgoing response data
  • Ensures sensitive fields like password_hash never leak in responses

Service

  • Contains all business logic
  • Decides what is allowed, what happens, and in what order
  • Coordinates multiple repository calls within a single transaction
  • Raises appropriate exceptions on business rule violations
  • Modules communicate only through service interfaces — never directly through repositories or models of another module

Repository

  • The only layer that communicates with the database
  • Executes SQLAlchemy queries
  • Uses flush() instead of commit() — transaction control belongs to the service layer
  • Never contains business logic

Model

  • Defines database tables as Python classes
  • Declares relationships between tables
  • Contains only simple helper properties
  • Pivot tables (many-to-many) are defined in the model file of the owning module

Request Flow Example

POST /api/v1/auth/login

1. Router         receives { email, password }
2. LoginRequest   validates schema — correct format?
3. AuthService    is email registered? is password correct? is user active?
4. UserRepository SELECT user WHERE email = ?
5. AuthService    hash comparison, generate tokens
6. TokenRepository INSERT refresh token
7. UserRepository UPDATE last_login
8. LoginResponse  serialize user + tokens
9. Router         return 200 OK

Transaction Management

Repositories use flush() to stage changes without committing. The get_db() dependency in database.py owns the transaction lifecycle:

Request starts  → session opens
All operations  → flush() (staged, not committed)
Request succeeds → commit()
Exception raised → rollback()
Request ends    → session closes

This ensures that multiple repository calls within a single service method are always atomic.

Module Boundaries

Modules are organized into feature domains. Cross-domain communication happens only at the service level:

✅ cms.posts.service  → auth.users.service  (via dependency injection)
✅ cms.posts.service  → cms.media.repository (within same domain is acceptable)
❌ cms.posts.repository → auth.users.model  (never cross model boundaries)

This separation makes it possible to extract a domain into a separate service in the future with minimal refactoring.