Layered Architecture
When Everything Was in One Module
Before I understood layered architecture, my code looked like this in practice: a function that accepted HTTP parameters, validated them, ran a database query, formatted the result, and returned the response β all in the same block. Changing the database schema broke the HTTP layer. Changing the response format required touching the query logic.
Layered architecture gave me the first vocabulary for talking about what belonged where, and β more importantly β the rule about which direction dependencies were allowed to flow.
Table of Contents
What Is Layered Architecture?
Layered architecture organises code into horizontal layers stacked on top of each other. Each layer:
Has a specific responsibility
Can only call layers below it (dependencies flow downward)
Exposes a well-defined interface to the layer above it
Is unaware of the layer above it
From top to bottom:
Presentation knows about HTTP; knows nothing about business rules
Application orchestrates use cases; knows nothing about HTTP or SQL
Domain contains the business rules; knows nothing about persistence
Infrastructure handles technical concerns; knows nothing about use cases
The Dependency Rule
The key constraint: dependencies only point downward. The Presentation layer can depend on Application; Application can depend on Domain; Domain can depend on Infrastructure.
What it prevents:
Business logic importing from router/controller code
Domain entities importing SQLAlchemy models
Use cases hard-coding HTTP calls to external services
The rule makes each layer independently testable and replaceable.
Standard Layers
Presentation Layer
Handles I/O with the outside world. For a web service, this is the HTTP boundary β routing, request parsing, response serialisation, HTTP status codes, authentication middleware.
The presentation layer should contain zero business logic. If a controller function does anything beyond parsing input, calling a service, and formatting output, business logic has leaked.
Application Layer
Orchestrates use cases β sequences of operations that fulfil a specific business goal. An application layer function like create_order() calls domain services, coordinates repositories, and publishes events. It does not know whether the request came from HTTP or a message queue.
Domain Layer
The heart of the application. Contains entities, value objects, domain services, and business rules. In a pure layered model, this layer has no knowledge of infrastructure β it does not import SQLAlchemy, httpx, or any external library.
Infrastructure Layer
Implements the technical concerns that the domain layer defines abstractly: repositories that read/write to a database, HTTP clients, email senders, file storage. The infrastructure layer implements interfaces defined in the domain or application layer.
The Risk: Lasagna Code
The failure mode of layered architecture is lasagna code: so many layers of abstraction for simple operations that making any change requires touching five files, each adding no real value.
I have written lasagna code. A GET /products endpoint that went through:
ProductController β 2. ProductApplicationService β 3. ProductDomainService β 4. ProductRepository β 5. ProductDataMapper β database
For a simple query, that is too much ceremony. Three layers is usually sufficient. Add a fourth only when it genuinely contains distinct logic.
When Layers Are Enough
Layered architecture is sufficient when:
The application logic is straightforward and does not change frequently
There is one primary interface (HTTP API) β no need to support multiple adapters
The team is small and grasps the entire codebase
Domain rules are not complex enough to warrant domain-centric designs
When the domain becomes complex, consider Onion Architecture or Ports & Adapters which give the domain stronger isolation guarantees.
Practical Example: Inventory Module
Notice: the Product domain model has no SQLAlchemy imports. It is a plain dataclass. The ProductRepository lives in Infrastructure and handles the ORM mapping. The ReserveStockUseCase in Application orchestrates the operation. The router in Presentation handles HTTP concerns.
Comparison with Onion and Hexagonal
Dependency direction
Downward through all layers
Inward toward domain
Inward toward core, via ports
Domain position
Middle layer
Centre
Core
Infrastructure position
Bottom layer
Outermost ring
Adapter (pluggable)
Primary benefit
Separation of concerns
Domain isolation
Multiple adapters (UI, API, CLI)
Test strategy
Mock lower layers
Mock infrastructure
Mock adapters via port interfaces
Layered architecture is the simplest starting point. Onion and Hexagonal express the same dependency direction more rigorously and make the domain's independence more explicit.
Lessons Learned
The main rule is one rule: dependencies flow in one direction. Everything else follows.
Separate the domain model from the persistence model. A SQLAlchemy ORM class is infrastructure. A domain entity is not. Merging them exposes domain code to database concerns.
Thin presentation layer, thin application layer, rich domain. Complexity belongs in the domain, not in the router.
Layered architecture does not mean every layer must always exist. Simple CRUD endpoints do not need a domain layer with rich entities. Add complexity proportional to the actual domain complexity.
Last updated