1.7 KiB
1.7 KiB
Layering Rules
Summary
The dependency direction is fixed. Violating it is a design defect.
Allowed imports
interfaces/* -> domain/* (services, ports, models)
interfaces/* -> infra/adapters/* (only read-only value objects)
domain/* -> domain/* (same layer)
domain/* -> pipelines/* (call a pipeline via a port)
infra/adapters/* -> domain/ports/* (implement the interface)
main.py/init.py -> everything (composition root only)
Forbidden imports
domain/* -> interfaces/* (never)
domain/* -> fastapi/starlette (never)
infra/adapters/* -> interfaces/* (never)
interfaces/* -> infra/db/* (state lives in resources, not imports)
Why
- A pure domain is testable without a server, a database, or an event loop.
- Adapters can be swapped (in-memory fake, real database) without touching business logic.
- The HTTP edge stays thin and replaceable.
The port/adapter contract
A port is an abstract class or protocol declared in domain/<x>/ports/.
An adapter implements it in infra/adapters/. The composition root binds
the two.
domain/<resource>/ports/db.py -> repository port + document model
domain/<resource>/ports/service.py -> service + factory
infra/db/repos/<vendor>.py -> repository adapter factory
infra/adapters/service.py -> generic CRUD service base
Enforcement
- Keep every domain module importable with only the standard library and
Pydantic. If a domain import pulls in
fastapi, the layering is broken. - Reviewers (and CI) reject
import fastapiinsidedomain/orinfra/.