3.8 KiB
Backend — Clean Architecture + DDD (be0)
Incremental re-layering of the FastAPI monolith (be0/main.py, ~3.7k LOC) into
DDD bounded contexts. Strangler-fig: the monolith keeps running; each context is
peeled out and cut over one endpoint at a time. No big-bang rewrite.
Layers — the dependency rule points INWARD
api ─────────────► application ─────────────► domain ◄───────── infrastructure
(FastAPI, Pydantic) (use cases, ports) (pure model) (adapters: SQLAlchemy,
▲ argon2, jwt, mail, S3…)
shared_kernel
domain/<context>/— entities, value objects, domain services, repository ports, errors. Pure Python: no FastAPI, SQLAlchemy, jwt, argon2, aioboto3, oros.getenv.application/<context>/— use cases orchestrating the domain via ports (Protocols); DTOs. No framework imports. One module per use case.infrastructure/<context>/— adapters that implement the ports (SQLAlchemy repositories, Argon2 hasher, JWT issuer, SMTP mailer, rate limiter, audit sink) +persistence/(engine/session) + existingvector_db/(Qdrant).api/<context>/— FastAPI routers + Pydantic schemas + dependencies. The only layer that imports FastAPI and maps domain errors to HTTP.composition/— wires use cases from concrete adapters (constructor injection; no DI framework).shared_kernel/—Entity/AggregateRoot,ValueObject, and theDomainErrorhierarchy.
DomainError → HTTP (mapped only in the api layer)
ValidationError → 400 · AuthenticationError → 401 · AuthorizationError → 403 · NotFoundError → 404 · ConflictError → 409 · RateLimited → 429. Inner layers raise these, never HTTPException.
Bounded contexts & extraction order (strangler-fig)
Identity (1st) → Admin → AI → Evidence/Files → Initiative (resolve the dual-submission-model decision: initiatives/drafts vs application_workflow/application_artifacts) → Review (last — most globals-coupled).
Status
| Context | domain | application | infrastructure | api | live cut-over |
|---|---|---|---|---|---|
| Identity | ✅ tested | ✅ Login tested | ⏳ | ⏳ | ⏳ (needs DB) |
| Admin · AI · Files · Initiative · Review | ⚪ | ⚪ | ⚪ | ⚪ | ⚪ |
Identity domain + application are extracted verbatim (behavior-preserving) from src/auth_api.py and covered by 32 unit tests (tests/test_identity_domain.py, tests/test_authenticate_user.py) that run with no DB.
Per-endpoint cut-over procedure
- Extract pure rules →
domain+ write unit tests. ✅ (pattern established) - Write the use case →
application(ports + fakes-tested). ✅ (Login) - Implement adapters →
infrastructure(wrap the existing battle-tested primitives —auth_jwt,auth_mail,auth_rate_limit,PasswordHasher; do not rewrite security code). - Add the FastAPI router →
api, wired viacomposition. Mount under a parallel prefix first. - With the stack up (
docker compose up), run the DB-backed auth tests against the new router and confirm byte-parity with the old handler. - Replace the old route in
auth_api.pywith the new router inmain.py; re-run tests. Repeat untilauth_api.pyis empty → delete it.
Why infrastructure/api are deferred this chunk
The auth tests are DB-backed (INITIATIVE_DATABASE_URL + Postgres). With the stack down, the DB-touching adapters/router can't be verified, and an unverified swap of live auth is unsafe. So this chunk ships the verified pure layers (domain + application) + the dead-scaffold cleanup; steps 3–6 happen when the stack is up.