Domain Driven Design Aggregate Root Example in Python
Model an Order as an aggregate root with invariants enforced through methods, demonstrating DDD principles in Python.
Python code
71 linesfrom __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from uuid import uuid4
class Money:
def __init__(self, amount: float, currency: str = "USD"):
self.amount = amount
self.currency = currency
def __add__(self, other: Money) -> Money:
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
def __repr__(self) -> str:
return f"${self.amount:.2f} {self.currency}"
@dataclass
class OrderItem:
product_id: str
quantity: int
price: Money
def total(self) -> Money:
return Money(self.price.amount * self.quantity, self.price.currency)
class Order:
def __init__(self, customer_id: str):
self.order_id = str(uuid4())[:8]
self.customer_id = customer_id
self.items: List[OrderItem] = []
self.status = "pending"
def add_item(self, product_id: str, quantity: int, price: Money) -> None:
if self.status != "pending":
raise ValueError("Order is no longer editable")
self.items.append(OrderItem(product_id, quantity, price))
def calculate_total(self) -> Money:
total = Money(0.0)
for item in self.items:
total = total + item.total()
return total
def submit(self) -> None:
if not self.items:
raise ValueError("Cannot submit empty order")
if self.status != "pending":
raise ValueError("Order already submitted")
self.status = "submitted"
def main() -> None:
order = Order(customer_id="CUST-123")
order.add_item("PROD-SHOES", 2, Money(59.99))
order.add_item("PROD-SOCKS", 3, Money(4.50))
print(f"Order {order.order_id} total: {order.calculate_total()}")
order.submit()
print(f"Order status: {order.status}")
try:
order.add_item("PROD-HAT", 1, Money(19.99))
except ValueError as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Output
Order 1a2b3c4d total: $133.48 USD
Order status: submitted
Error: Order is no longer editable
How it works
The Order class acts as an aggregate root, owning a collection of OrderItem values and enforcing business rules through its methods. Invariants like 'cannot add items after submission' are checked before mutations, keeping the aggregate consistent. Money operations validate currency to prevent invalid calculations. This pattern keeps the domain logic encapsulated and the model safe from invalid states.
Common mistakes
- Exposing internal collections as mutable lists, allowing external code to bypass invariants
- Not validating state transitions, leading to orders being modified after submission
- Mixing value objects like Money with entities without proper encapsulation
Variations
- Use dataclasses with `frozen=True` for value objects to enforce immutability
- Implement the aggregate as a class with private fields and explicit methods for state changes
Real-world use cases
- E-commerce order processing where order lifecycle must prevent invalid state changes
- Banking account aggregates enforcing balance rules on deposits and withdrawals
- Inventory management with stock levels as aggregates to prevent overselling
Sponsored
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Facade Pattern in Python with Mock Simplification medium
- How to Aggregate Mock API Routes by Method in Python easy
Keep learning
Related tutorials and quizzes for this topic.