Skip to main content

Clean Architecture

Backend applications use Clean Architecture with hexagonal ports and adapters, organized by feature. The goal is to keep business rules independent of frameworks, databases, and delivery mechanisms so they stay testable and replaceable.

Layers​

LayerHoldsDepends on
DomainEntities, value objects, aggregates, domain services, validation rules, repository ports (Domain modeling)Nothing outside itself
ApplicationUse cases, DTOs, mappers to DTOs, transaction boundaries, ports for non-domain collaboratorsDomain
InfrastructureDatabase models, repositories, HTTP clients, adaptersDomain, application ports
PresentationRouters, request and response schemasApplication
Composition rootWiring of ports to implementationsAll of the above

A port lives in the layer that needs it. Persistence ports describe how the domain stores and loads entities, so they live in domain. Ports for external collaborators that only a use case needs, such as an email sender or a payment client, live in application.

Principles​

  • Dependency rule: source dependencies point inward. Inner layers never import outer layers.
  • Ports and adapters: the domain or application layer defines an interface (a port); infrastructure supplies the implementation (an adapter, see Design patterns). Only the composition root knows which adapter is used.
  • Feature-first (details): each feature owns its layers, so a feature can be understood, tested, and changed in one place. Code moves to shared/ only after real reuse.
  • Explicit mapping: domain entities, persistence models, and API DTOs are separate types. Mapping between them is explicit, in standalone mappers.
  • Pragmatism: apply KISS and YAGNI before DRY and SOLID. Do not add a layer, interface, or pattern that solves a problem you do not have. See Design principles.

Request flow​

A request passes from the router to the use case to the adapter and back. Entities never reach the router, and the use case never sees HTTP. The composition root injected the adapter behind the port.

Testing by layer​

Each layer has one natural test type, so tests stay fast and failures point to one place. See testing strategy.

LayerTest typeNotes
DomainUnitPure logic, no fakes needed
ApplicationUnitUse in-memory fakes for ports, not mocks of internals
InfrastructureIntegrationReal database or service; verifies mapping and queries
PresentationAPIFull request path through the router
Layer boundariesArchitecture testParses imports and fails on forbidden ones (backend testing)

Common violations​

ViolationWhy it hurtsFix
Business rule in a routerCannot be reused or unit tested without HTTPMove it to the domain or a use case
ORM model returned from a route or used in a use caseThe database schema leaks into the API and the domainMap to an entity or DTO in a mapper
Use case constructs a repository or clientHidden dependency, no way to substitute it in testsReceive the port through the composition root
Domain imports SQLAlchemy, FastAPI, or PydanticThe domain stops being independent of frameworksKeep framework types in infrastructure and presentation
Generic BaseRepository with CRUD for every entityExposes operations the domain does not needWrite an explicit port per feature
Pass-through layer that only forwards callsIndirection with no behaviorRemove it or call the shared function directly

When not to use this structure​

Do not apply this structure to small scripts, CRUD-only prototypes with no business rules, or repositories that follow an established different architecture. The layers pay off only when there are business rules to protect; without them they are indirection (YAGNI).

Concrete folder layout and rules: FastAPI structure.