48 lines
3.8 KiB
Markdown
48 lines
3.8 KiB
Markdown
# 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, or `os.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) + existing `vector_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 the `DomainError` hierarchy.
|
||
|
||
### 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
|
||
1. Extract pure rules → `domain` + write unit tests. ✅ (pattern established)
|
||
2. Write the use case → `application` (ports + fakes-tested). ✅ (Login)
|
||
3. Implement adapters → `infrastructure` (wrap the existing battle-tested primitives — `auth_jwt`, `auth_mail`, `auth_rate_limit`, `PasswordHasher`; do **not** rewrite security code).
|
||
4. Add the FastAPI router → `api`, wired via `composition`. Mount under a parallel prefix first.
|
||
5. **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.
|
||
6. Replace the old route in `auth_api.py` with the new router in `main.py`; re-run tests. Repeat until `auth_api.py` is 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.
|