Business Logic Guide

Overview

How we split HTTP, services, repositories, and models.

Layering model

Rule:

Theoretical background

Layered architecture — theoretical background

Layered architecture stacks the system in horizontal layers. Each layer talks only to the layer below. Inner layers (rules, persistence) do not import HTTP or transport details, so you can test and swap adapters.

This is close to ports and adapters: the core defines behavior; HTTP and SQL are adapters.

Contract-first means OpenAPI describes the HTTP surface; implementation must match. The outer adapter stays tied to a stable contract.

Layer responsibilities (reference)

The public API is described first in OpenAPI (“contract-first”): what you see in the spec is what callers should rely on. System-wide diagrams (C4) live in System design; a simplified request-path diagram is rendered from docs/uml/architecture/internal_readme_layer_boundaries.puml.

Layer Packages Responsibility Contract and constraints
HTTP API app/api/ FastAPI routers: declare paths, dependencies, and Pydantic-typed bodies/responses from app/schemas/; call services. Handlers stay thin (no duplicate field validation logic here). Must match OpenAPI (operations, declared responses, examples). Maps successful outcomes and framework-level HTTP concerns to the right status codes.
Schemas & 422 contract app/schemas/, app/validation/, handlers in app/main.py app/schemas/: Pydantic models and field constraints. app/validation/: translates Pydantic/FastAPI validation failures into the canonical 422 payload shape for clients. Registration of exception handlers lives next to app setup in app/main.py. Error catalog and examples in OpenAPI; alignment between Pydantic error kinds and documented code/key values.
Application app/services/ Use cases and business invariants (for example “duplicate composite key”) implemented with domain logic; raises HTTPException with business error payloads when rules fail. Business errors must match the governed contract. Layering policy: Developers Docs.
Persistence app/repositories/, DB session in app/core/ Load and persist entities via SQLAlchemy; Alembic migrations for schema changes. Storage details stay out of the public HTTP model; DB constraints remain consistent with what the API promises.

Where validation runs

Where checks happen (in order):

  1. Field rules for JSON bodies are defined in Pydantic models under app/schemas/ (required fields, types, max length, and so on).
  2. When a request arrives, FastAPI runs those checks before code in app/api/ runs. Handlers usually receive data that already passed this step.
  3. If a check fails, app/validation/ maps the failure to our standard 422 response (code, key, message). It does not redefine the rules—only the error shape for clients. That wiring is registered in app/main.py.
  4. Rules that need the database—such as “this user already exists”—are enforced in app/services/ and surface as business client errors, not as “invalid JSON field” / schema validation. See ADR 0003.

Responsibility boundaries

Router:

Service:

Repository:

Transactions and consistency

Logging and observability

Typical change flow

  1. Define or adjust schema.
  2. Implement service rule.
  3. Implement repository operation.
  4. Wire router.
  5. Add examples and error mappings.
  6. Add tests.
  7. Run make pipeline.

Page history

Date Change Author
Added Page history section (repository baseline). Ivan Boyarkin