# 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?](#what-is-layered-architecture)
* [The Dependency Rule](#the-dependency-rule)
* [Standard Layers](#standard-layers)
* [The Risk: Lasagna Code](#the-risk-lasagna-code)
* [When Layers Are Enough](#when-layers-are-enough)
* [Practical Example: Inventory Module](#practical-example-inventory-module)
* [Comparison with Onion and Hexagonal](#comparison-with-onion-and-hexagonal)
* [Lessons Learned](#lessons-learned)

***

## 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

{% @mermaid/diagram content="graph TB
P\["🖥️  Presentation Layer<br/>(HTTP, CLI, WebSocket)"]
A\["⚙️  Application Layer<br/>(Use Cases, Orchestration)"]
D\["🏛️  Domain Layer<br/>(Business Rules, Entities)"]
I\["🗄️  Infrastructure Layer<br/>(Database, HTTP Clients, File I/O)"]

```
P --> A
A --> D
D --> I

style P fill:#d4edda
style A fill:#cce5ff
style D fill:#fff3cd
style I fill:#f8d7da" %}
```

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:

1. 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](https://blog.htunnthuthu.com/architecture-and-design/architecture-and-patterns/software-architecture-101/application-patterns/02-onion-architecture) or [Ports & Adapters](https://blog.htunnthuthu.com/architecture-and-design/architecture-and-patterns/software-architecture-101/application-patterns/03-ports-and-adapters) which give the domain stronger isolation guarantees.

***

## Practical Example: Inventory Module

```python
# --- Domain Layer ---
# inventory/domain/models.py

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    tenant_id: str
    name: str
    sku: str
    stock_quantity: int
    minimum_stock: int

    def is_low_stock(self) -> bool:
        return self.stock_quantity <= self.minimum_stock

    def reserve(self, quantity: int) -> None:
        if quantity > self.stock_quantity:
            raise ValueError(
                f"Cannot reserve {quantity}; only {self.stock_quantity} available"
            )
        self.stock_quantity -= quantity
```

```python
# --- Infrastructure Layer ---
# inventory/infrastructure/repository.py

from sqlalchemy.orm import Session
from ..domain.models import Product
from .orm import ProductORM  # SQLAlchemy ORM model — separate from domain model

class ProductRepository:
    def __init__(self, db: Session):
        self._db = db

    def get(self, tenant_id: str, product_id: int) -> Product | None:
        row = (
            self._db.query(ProductORM)
            .filter_by(tenant_id=tenant_id, id=product_id)
            .first()
        )
        if not row:
            return None
        return Product(
            id=row.id,
            tenant_id=row.tenant_id,
            name=row.name,
            sku=row.sku,
            stock_quantity=row.stock_quantity,
            minimum_stock=row.minimum_stock,
        )

    def save(self, product: Product) -> None:
        self._db.query(ProductORM).filter_by(id=product.id).update({
            "stock_quantity": product.stock_quantity
        })
        self._db.commit()
```

```python
# --- Application Layer ---
# inventory/application/use_cases.py

from ..domain.models import Product
from ..infrastructure.repository import ProductRepository

class ReserveStockUseCase:
    def __init__(self, repo: ProductRepository):
        self._repo = repo

    def execute(self, tenant_id: str, product_id: int, quantity: int) -> Product:
        product = self._repo.get(tenant_id, product_id)
        if product is None:
            raise ValueError(f"Product {product_id} not found")

        product.reserve(quantity)  # Domain rule enforced here
        self._repo.save(product)
        return product
```

```python
# --- Presentation Layer ---
# inventory/presentation/router.py

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from ..application.use_cases import ReserveStockUseCase
from ..infrastructure.repository import ProductRepository
from .schemas import ReserveRequest, ProductResponse

router = APIRouter()

@router.post("/reserve", response_model=ProductResponse)
def reserve_stock(body: ReserveRequest, db: Session = Depends(get_db)):
    use_case = ReserveStockUseCase(ProductRepository(db))
    try:
        product = use_case.execute(body.tenant_id, body.product_id, body.quantity)
        return ProductResponse.from_domain(product)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
```

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

| Aspect                  | Layered                     | Onion                | Hexagonal (Ports & Adapters)      |
| ----------------------- | --------------------------- | -------------------- | --------------------------------- |
| 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.
