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:
from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModel
app = FastAPI()
# DTO (Data Transfer Object) for external APIclass 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")@RestController@RequestMapping("/api/orders")public class OrderController {
private final OrderService orderService;
// DTO for external API public record CreateOrderRequest( String customerId, List<ItemRequest> items, Address shippingAddress ) {}
public record OrderResponse( String orderId, String status, BigDecimal total ) {}
@PostMapping public ResponseEntity<OrderResponse> createOrder( @RequestBody CreateOrderRequest request ) { try { // Delegate to application layer Order order = orderService.placeOrder( request.customerId(), request.items(), request.shippingAddress() );
// Convert domain model to DTO OrderResponse response = new OrderResponse( order.getId(), order.getStatus().toString(), order.getTotal() );
return ResponseEntity.ok(response); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().build(); } catch (Exception e) { return ResponseEntity.internalServerError().build(); } }}Key Point: Presentation layer is thin - no business logic!
Responsibility: Coordinate application flow and transactions
Contains:
What it does:
from dataclasses import dataclassfrom decimal import Decimal
@dataclassclass 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@Service@Transactionalpublic class OrderService { /** * Application service - orchestrates use cases */
private final OrderRepository orderRepo; private final InventoryService inventory; private final PaymentService payment; private final NotificationService notifications;
public Order placeOrder( String customerId, List<ItemRequest> items, Address address ) { // 1. Check inventory (domain service) boolean available = inventory.checkAvailability(items); if (!available) { throw new IllegalStateException("Some items out of stock"); }
// 2. Create order (domain logic) Order order = Order.createNew(customerId, items, address);
// 3. Reserve inventory (transaction coordination) inventory.reserve(order.getItems());
try { // 4. Process payment PaymentResult result = payment.process( order.getId(), order.getTotal() );
if (!result.isSuccess()) { // Compensate: release inventory inventory.release(order.getItems()); throw new PaymentFailedException("Payment failed"); }
// 5. Confirm order (domain logic) order.confirm();
// 6. Save (transaction boundary - @Transactional) orderRepo.save(order);
// 7. Send notification (side effect) notifications.notifyOrderCreated(order);
return order;
} catch (Exception e) { // Compensate: release inventory inventory.release(order.getItems()); throw e; } }}Key Point: Application layer orchestrates, domain layer decides!
Responsibility: Core business logic and rules
Contains:
What it does:
from dataclasses import dataclass, fieldfrom decimal import Decimalfrom enum import Enumfrom typing import List
class OrderStatus(Enum): DRAFT = "draft" CONFIRMED = "confirmed" PAID = "paid" SHIPPED = "shipped" DELIVERED = "delivered" CANCELLED = "cancelled"
@dataclassclass 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 ))public class Order { /** * Domain entity - contains business logic */
private final String id; private final String customerId; private final List<OrderLine> lines; private OrderStatus status; private final List<DomainEvent> events;
// Factory method with business rules public static Order createNew( String customerId, List<ItemRequest> items, Address address ) { if (customerId == null || customerId.isEmpty()) { throw new IllegalArgumentException("Customer ID required"); } if (items == null || items.isEmpty()) { throw new IllegalArgumentException("Order must have items"); }
Order order = new Order(generateOrderId(), customerId);
for (ItemRequest item : items) { order.addItem( item.productId(), item.name(), item.quantity(), item.price() ); }
return order; }
public void addItem( String productId, String productName, int quantity, BigDecimal unitPrice ) { // Business rule: Can only modify draft orders if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Cannot modify confirmed order"); }
if (quantity <= 0) { throw new IllegalArgumentException("Quantity must be positive"); }
// Check if item already in order for (OrderLine line : lines) { if (line.getProductId().equals(productId)) { line.increaseQuantity(quantity); return; } }
// Add new line lines.add(new OrderLine( productId, productName, quantity, unitPrice )); }
public BigDecimal calculateTotal() { return lines.stream() .map(OrderLine::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); }
public void confirm() { // Business rule: Order confirmation if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Only draft orders can be confirmed"); }
if (lines.isEmpty()) { throw new IllegalStateException("Cannot confirm empty order"); }
if (calculateTotal().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalStateException("Order total must be positive"); }
status = OrderStatus.CONFIRMED;
// Domain event events.add(new OrderConfirmedEvent( id, customerId, calculateTotal() )); }
public void cancel(String reason) { // Business rule: Order cancellation if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) { throw new IllegalStateException("Cannot cancel shipped order"); }
status = OrderStatus.CANCELLED;
// Domain event events.add(new OrderCancelledEvent(id, reason)); }}Key Point: Domain layer is pure - no dependencies on infrastructure!
Responsibility: Technical implementation details
Contains:
What it does:
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@Repositorypublic class JpaOrderRepository implements OrderRepository { /** * Infrastructure implementation of domain interface */
@PersistenceContext private EntityManager entityManager;
@Autowired private EventBus eventBus;
@Override public void save(Order order) { // Map domain entity to JPA entity OrderEntity entity = new OrderEntity(); entity.setId(order.getId()); entity.setCustomerId(order.getCustomerId()); entity.setStatus(order.getStatus().toString()); entity.setTotal(order.calculateTotal());
// Save order lines for (OrderLine line : order.getLines()) { OrderLineEntity lineEntity = new OrderLineEntity(); lineEntity.setOrderId(order.getId()); lineEntity.setProductId(line.getProductId()); lineEntity.setProductName(line.getProductName()); lineEntity.setQuantity(line.getQuantity()); lineEntity.setUnitPrice(line.getUnitPrice()); entity.getLines().add(lineEntity); }
entityManager.persist(entity); entityManager.flush();
// Publish domain events for (DomainEvent event : order.getEvents()) { eventBus.publish(event); } }
@Override public Optional<Order> findById(String orderId) { // Query database OrderEntity entity = entityManager.find(OrderEntity.class, orderId);
if (entity == null) { return Optional.empty(); }
// Map JPA entity to domain entity Order order = new Order( entity.getId(), entity.getCustomerId() ); order.setStatus(OrderStatus.valueOf(entity.getStatus()));
for (OrderLineEntity lineEntity : entity.getLines()) { order.getLines().add(new OrderLine( lineEntity.getProductId(), lineEntity.getProductName(), lineEntity.getQuantity(), lineEntity.getUnitPrice() )); }
return Optional.of(order); }}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.