54 lines
1.7 KiB
Markdown
54 lines
1.7 KiB
Markdown
# 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 fastapi` inside `domain/` or `infra/`.
|