Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Layered Architecture

Organizing complexity through well-defined layers and dependencies

Imagine building a cake with distinct layers:

  • Top layer: Frosting and decorations (what people see)
  • Middle layer: Cake itself (the substance)
  • Bottom layer: The plate (what holds everything)

Each layer has a specific job, and you build from bottom to top. That’s layered architecture - organizing software into horizontal layers where each layer has a specific responsibility!


Diagram

Responsibility: Interface with the outside world

Contains:

  • Web controllers
  • REST API endpoints
  • GraphQL resolvers
  • Views/templates
  • Request/response DTOs
  • Input validation

What it does:

  • Accept user input
  • Display information
  • Handle HTTP/request-response cycle
  • Convert between external formats and application models
presentation/order_controller.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# DTO (Data Transfer Object) for external API
class CreateOrderRequest(BaseModel):
customer_id: str
items: list[dict]
shipping_address: dict
class OrderResponse(BaseModel):
order_id: str
status: str
total: float
@app.post("/api/orders", response_model=OrderResponse)
async def create_order(request: CreateOrderRequest):
"""Presentation layer - handles HTTP"""
try:
# Delegate to application layer
order = await order_service.place_order(
customer_id=request.customer_id,
items=request.items,
address=request.shipping_address
)
# Convert domain model to DTO
return OrderResponse(
order_id=order.id,
status=order.status,
total=float(order.total)
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Internal error")

Key Point: Presentation layer is thin - no business logic!

Responsibility: Coordinate application flow and transactions

Contains:

  • Application services
  • Use case implementations
  • Transaction boundaries
  • Security/authorization
  • DTOs and mappers

What it does:

  • Orchestrate domain objects
  • Define transaction boundaries
  • Coordinate between domain and infrastructure
  • Implement application-specific logic (not business rules!)
application/order_service.py
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class PlaceOrderCommand:
"""Application-level command"""
customer_id: str
items: list[dict]
shipping_address: dict
class OrderService:
"""Application service - orchestrates use cases"""
def __init__(
self,
order_repo: OrderRepository,
inventory_service: InventoryService,
payment_service: PaymentService,
notification_service: NotificationService
):
self._order_repo = order_repo
self._inventory = inventory_service
self._payment = payment_service
self._notifications = notification_service
async def place_order(
self,
customer_id: str,
items: list,
address: dict
) -> Order:
"""Use case: Customer places an order"""
# 1. Check inventory (domain service)
available = await self._inventory.check_availability(items)
if not available:
raise ValueError("Some items out of stock")
# 2. Create order (domain logic)
order = Order.create_new(
customer_id=customer_id,
items=items,
shipping_address=address
)
# 3. Reserve inventory (transaction coordination)
await self._inventory.reserve(order.items)
try:
# 4. Process payment
payment_result = await self._payment.process(
order.id,
order.total
)
if not payment_result.success:
# Compensate: release inventory
await self._inventory.release(order.items)
raise ValueError("Payment failed")
# 5. Confirm order (domain logic)
order.confirm()
# 6. Save (transaction boundary)
await self._order_repo.save(order)
# 7. Send notification (side effect)
await self._notifications.notify_order_created(order)
return order
except Exception as e:
# Compensate: release inventory
await self._inventory.release(order.items)
raise

Key Point: Application layer orchestrates, domain layer decides!

Responsibility: Core business logic and rules

Contains:

  • Entities
  • Value objects
  • Domain services
  • Domain events
  • Business rules
  • Invariants

What it does:

  • Enforce business rules
  • Maintain consistency
  • Express business concepts
  • Pure business logic (no infrastructure!)
domain/order.py
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import List
class OrderStatus(Enum):
DRAFT = "draft"
CONFIRMED = "confirmed"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
@dataclass
class OrderLine:
"""Value object"""
product_id: str
product_name: str
quantity: int
unit_price: Decimal
def subtotal(self) -> Decimal:
return self.unit_price * self.quantity
class Order:
"""Domain entity - contains business logic"""
def __init__(
self,
id: str,
customer_id: str,
lines: List[OrderLine] = None
):
self.id = id
self.customer_id = customer_id
self.lines = lines or []
self.status = OrderStatus.DRAFT
self._events = []
@staticmethod
def create_new(customer_id: str, items: list, address: dict):
"""Factory method with business rules"""
if not customer_id:
raise ValueError("Customer ID required")
if not items:
raise ValueError("Order must have items")
order = Order(
id=generate_order_id(),
customer_id=customer_id
)
for item in items:
order.add_item(
product_id=item['product_id'],
product_name=item['name'],
quantity=item['quantity'],
unit_price=Decimal(item['price'])
)
return order
def add_item(
self,
product_id: str,
product_name: str,
quantity: int,
unit_price: Decimal
):
"""Business rule: Can only modify draft orders"""
if self.status != OrderStatus.DRAFT:
raise ValueError("Cannot modify confirmed order")
if quantity <= 0:
raise ValueError("Quantity must be positive")
# Check if item already in order
for line in self.lines:
if line.product_id == product_id:
line.quantity += quantity
return
# Add new line
self.lines.append(OrderLine(
product_id=product_id,
product_name=product_name,
quantity=quantity,
unit_price=unit_price
))
def calculate_total(self) -> Decimal:
"""Business calculation"""
return sum(line.subtotal() for line in self.lines)
def confirm(self):
"""Business rule: Order confirmation"""
if self.status != OrderStatus.DRAFT:
raise ValueError("Only draft orders can be confirmed")
if not self.lines:
raise ValueError("Cannot confirm empty order")
if self.calculate_total() <= 0:
raise ValueError("Order total must be positive")
self.status = OrderStatus.CONFIRMED
# Domain event
self._events.append(OrderConfirmedEvent(
order_id=self.id,
customer_id=self.customer_id,
total=self.calculate_total()
))
def cancel(self, reason: str):
"""Business rule: Order cancellation"""
if self.status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED]:
raise ValueError("Cannot cancel shipped order")
self.status = OrderStatus.CANCELLED
# Domain event
self._events.append(OrderCancelledEvent(
order_id=self.id,
reason=reason
))

Key Point: Domain layer is pure - no dependencies on infrastructure!

4. Infrastructure Layer (Technical Details)

Section titled “4. Infrastructure Layer (Technical Details)”

Responsibility: Technical implementation details

Contains:

  • Database access (repositories)
  • External API clients
  • File system access
  • Message queue clients
  • Email/SMS sending
  • Caching
  • Logging

What it does:

  • Implement interfaces defined by domain/application layers
  • Handle persistence
  • Integrate with external systems
  • Technical concerns (caching, logging, etc.)
infrastructure/order_repository.py
from sqlalchemy.orm import Session
class PostgresOrderRepository(OrderRepository):
"""Infrastructure implementation of domain interface"""
def __init__(self, db_session: Session):
self._session = db_session
async def save(self, order: Order) -> None:
"""Persist domain entity to database"""
# Map domain entity to database model
db_order = OrderModel(
id=order.id,
customer_id=order.customer_id,
status=order.status.value,
total=order.calculate_total()
)
# Save order lines
for line in order.lines:
db_line = OrderLineModel(
order_id=order.id,
product_id=line.product_id,
product_name=line.product_name,
quantity=line.quantity,
unit_price=line.unit_price
)
db_order.lines.append(db_line)
self._session.add(db_order)
await self._session.commit()
# Publish domain events
for event in order.get_events():
await event_bus.publish(event)
async def find_by_id(self, order_id: str) -> Optional[Order]:
"""Load domain entity from database"""
# Query database
db_order = await self._session.query(OrderModel)\
.filter_by(id=order_id)\
.first()
if not db_order:
return None
# Map database model to domain entity
order = Order(
id=db_order.id,
customer_id=db_order.customer_id
)
order.status = OrderStatus(db_order.status)
for db_line in db_order.lines:
order.lines.append(OrderLine(
product_id=db_line.product_id,
product_name=db_line.product_name,
quantity=db_line.quantity,
unit_price=db_line.unit_price
))
return order

Key Point: Infrastructure implements interfaces, doesn’t define them!


Dependencies point INWARD (toward domain):

Diagram

Domain defines interfaces, infrastructure implements:

# Domain layer defines what it needs
class OrderRepository(ABC):
"""Domain interface - no implementation details!"""
@abstractmethod
async def save(self, order: Order):
pass
@abstractmethod
async def find_by_id(self, order_id: str) -> Optional[Order]:
pass
# Infrastructure layer provides implementation
class PostgresOrderRepository(OrderRepository):
"""Infrastructure implements domain interface"""
async def save(self, order: Order):
# PostgreSQL-specific code
pass
# Application layer uses abstraction
class OrderService:
def __init__(self, order_repo: OrderRepository): # Depends on interface
self._repo = order_repo

Separation of Concerns

Each layer has clear responsibility. UI doesn’t know about database, business logic doesn’t know about HTTP.

Testability

Test domain logic without database. Test application logic without HTTP. Mock dependencies easily.

Maintainability

Changes localized to specific layers. Replace database without touching business logic.

Team Organization

Different teams can own different layers. Frontend team owns presentation, backend owns domain/application.


Problem: Domain layer becomes just data containers, all logic in application layer

# BAD: Anemic domain model
class Order:
"""Just data, no behavior"""
id: str
customer_id: str
items: list
total: Decimal
class OrderService:
"""All logic in application service"""
def calculate_total(self, order):
return sum(item.price * item.quantity for item in order.items)
def can_be_cancelled(self, order):
return order.status in ["draft", "confirmed"]
# GOOD: Rich domain model
class Order:
"""Data + behavior"""
def calculate_total(self) -> Decimal:
return sum(line.subtotal() for line in self.lines)
def cancel(self):
if not self._can_be_cancelled():
raise ValueError("Order cannot be cancelled")
self.status = OrderStatus.CANCELLED
def _can_be_cancelled(self) -> bool:
return self.status in [OrderStatus.DRAFT, OrderStatus.CONFIRMED]

Problem: Abstractions leak between layers

# BAD: Infrastructure leaking to presentation
@app.get("/orders/{order_id}")
async def get_order(order_id: str, db: Session): # Database session in controller!
order = db.query(OrderModel).filter_by(id=order_id).first()
return order
# GOOD: Proper layering
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
order = await order_service.get_order(order_id) # Use application layer
return OrderResponse.from_domain(order)

Problem: Skipping layers creates unnecessary code

# Sometimes OK to skip application layer for simple queries
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
# Simple query - can go directly to repository
order = await order_repository.find_by_id(order_id)
return OrderResponse.from_domain(order)
# Use application layer for complex operations
@app.post("/orders")
async def create_order(request: CreateOrderRequest):
# Complex workflow - must use application layer
order = await order_service.place_order(...)
return OrderResponse.from_domain(order)

  1. Building Traditional Enterprise Applications

    • Clear separation between UI, business logic, database
    • Well-understood requirements
    • Team familiar with pattern
  2. Domain Logic is Complex

    • Need to isolate business rules
    • Many business invariants to maintain
    • Domain experts involved
  3. Long-Lived Applications

    • Expect to maintain for years
    • Technology will change (replace database, UI framework)
    • Team members will change
  4. Monolithic Applications

    • Single deployment unit
    • Shared database
    • Clear layer boundaries
  1. Microservices

    • Each service is small enough that layers add overhead
    • Consider vertical slices instead
  2. Simple CRUD Applications

    • No complex business logic
    • Just reading/writing data
    • Layers add unnecessary complexity
  3. Event-Driven Systems

    • Consider hexagonal architecture instead
    • More flexible for async communication

Dependencies Flow Inward

Always depend on inner layers. Domain never depends on infrastructure or presentation!

Rich Domain Models

Put business logic in domain entities, not in services. Avoid anemic domain models.

Interface Segregation

Domain defines interfaces, infrastructure implements. Use dependency inversion!

Pragmatic Layering

Follow the spirit, not the letter. Skip layers when it makes sense for simple operations.



  • “Domain-Driven Design” by Eric Evans - Rich domain models
  • “Clean Architecture” by Robert C. Martin - Modern layering
  • “Patterns of Enterprise Application Architecture” by Martin Fowler
  • “Implementing Domain-Driven Design” by Vaughn Vernon