Files
sciagent/docs/backend-clean-architecture.md
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

3.8 KiB
Raw Permalink Blame History

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 36 happen when the stack is up.