Skip to content

Clean architecture audit fixes: align declared and actual architecture - #39

Merged
MartinKalema merged 1 commit into
mainfrom
refactor/clean-architecture-audit
Jul 6, 2026
Merged

Clean architecture audit fixes: align declared and actual architecture#39
MartinKalema merged 1 commit into
mainfrom
refactor/clean-architecture-audit

Conversation

@MartinKalema

Copy link
Copy Markdown
Owner

Summary

An Uncle Bob-perspective audit found the runtime dependency rule held perfectly (zero framework imports inward of infrastructure, mechanically verified) — but the type layer told a different story: annotations violated the rule, most ports had no dependents, and dead machinery misled readers. This PR makes the declared architecture and the actual architecture the same thing, and adds the enforcement that keeps them that way.

Dependency Rule

  • CQRS query ports move to the application layer (application/query_handlers/interfaces.py) — they serve views and return application read models, so the audit's only true inward-ring violation (domain importing BookReadModel) disappears. Write-side ports stay in the domain.
  • All 17 handler annotations re-pointed from concrete adapters to ports (ILoanUnitOfWork, ICache, query ports); 8 phantom imports of non-existent names fixed
  • sqlalchemy leaves presentation: IntegrityError on the unique-active-loan index is translated to BookNotAvailableException inside the loan unit of work
  • Health routes decoupled: PostgreSQL.ping(), circuit-breaker registry provided by the container

Boundary contracts

  • Read models consolidated (each of PatronReadModel/LoanReadModel was defined twice); patron/loan query repositories now return typed read models instead of raw dicts
  • ICache protocol matches the adapter's real async surface

Main component

  • api/main.py is now a create_app() factory (uvicorn --factory): importing the module performs no I/O, so the non-e2e suite runs with zero infrastructure (verified with etcd unreachable)

Dead architecture removed

BookRepository, ICatalogQueryRepository, IPatronRepository, EmailTemplate, ITemplateRenderer — all zero-use.

Enforcement (the part that keeps it fixed)

  • mypy clean on 134 files, configured in pyproject; SQLAlchemy legacy-Column noise scoped to persistence adapters with a documented override
  • GitHub Actions CI: mypy + the infrastructure-free test suites on every push/PR
  • mypy immediately paid for itself with two real defects: three patron routes dereferencing a possibly-None query result (500 on a missing patron), and the reservation reaper calling find_expired_reservations which existed on the concrete repository but not on its port

Verification

  • 157 tests passing; mechanical dependency sweep now: domain→outward 0, application→infra 0, sqlalchemy in presentation 0
  • Live: factory-composed app healthy behind nginx, borrow saga end-to-end, health endpoints working through the container-provided registry

… fixes)

A clean-architecture audit found the runtime dependency rule held
everywhere, but the type layer told a different story. This makes the
two match, and adds the enforcement that keeps them matched.

Dependency Rule:
- CQRS query ports move from domain/*/interfaces to the application
  layer (application/query_handlers/interfaces.py): they serve views and
  return application read models, so the domain importing BookReadModel
  (the audit's only true inward-ring violation) disappears. Write-side
  ports (command repositories, units of work) stay in the domain.
- All 17 handler annotations re-pointed from concrete adapters
  (LoanUnitOfWork, CacheAdapter, PatronQueryRepository) to ports
  (ILoanUnitOfWork, ICache, application query ports); 8 phantom
  TYPE_CHECKING imports of non-existent names fixed
- sqlalchemy leaves the presentation layer: the loan unit of work
  translates IntegrityError on the unique-active-loan index into
  BookNotAvailableException at the boundary
- Health routes stop importing infrastructure directly: PostgreSQL
  gains ping(), the circuit-breaker registry is container-provided

Boundary contracts:
- PatronReadModel/LoanReadModel were each defined twice; all read models
  consolidated in application/query_handlers/read_models.py
- Patron and loan query repositories return typed read models instead of
  raw dicts, matching the book convention end to end
- ICache protocol updated to the adapter's real (async) surface

Main component:
- api/main.py becomes a create_app() factory (uvicorn --factory);
  importing the module no longer builds the container or calls etcd,
  so the non-e2e test suite runs with zero infrastructure

Dead architecture removed:
- BookRepository, ICatalogQueryRepository, IPatronRepository,
  EmailTemplate, ITemplateRenderer (all zero-use)

Enforcement:
- mypy configured in pyproject (SQLAlchemy legacy-Column noise scoped to
  persistence adapters, documented) and clean on 134 files
- GitHub Actions CI: mypy + the infrastructure-free test suites
- mypy immediately caught two real defects: three patron routes
  dereferencing a possibly-None query result (500 on missing patron),
  and the reaper calling find_expired_reservations which existed on the
  concrete repository but not on the port

157 tests passing; verified live (factory app, saga end-to-end, health
endpoints through the container-provided registry).
@MartinKalema
MartinKalema merged commit fc86182 into main Jul 6, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant