Separation of Concerns
Each layer has clear responsibility. UI doesn’t know about database, business logic doesn’t know about HTTP.
Imagine building a cake with distinct layers:
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!
Responsibility: Interface with the outside world
Contains:
What it does:
Key Point: Application layer orchestrates - coordinates between layers!
Key Point: Application layer orchestrates, domain layer decides!
Responsibility: Core business logic and rules
Contains:
What it does:
Key Point: Domain layer is pure - no dependencies on infrastructure!
Responsibility: Technical implementation details
Contains:
What it does:
Key Point: Infrastructure implements interfaces, doesn’t define them!
Dependencies point INWARD (toward domain):
Domain defines interfaces, infrastructure implements:
# Domain layer defines what it needsclass 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 implementationclass PostgresOrderRepository(OrderRepository): """Infrastructure implements domain interface""" async def save(self, order: Order): # PostgreSQL-specific code pass
# Application layer uses abstractionclass OrderService: def __init__(self, order_repo: OrderRepository): # Depends on interface self._repo = order_repoSeparation 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 modelclass 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 modelclass 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)Building Traditional Enterprise Applications
Domain Logic is Complex
Long-Lived Applications
Monolithic Applications
Microservices
Simple CRUD Applications
Event-Driven Systems
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.