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.

Medium Python 3.9+ Aug 9, 2026 System design patterns 12 views 0 copies

Python code

71 lines
Python 3.9+
from __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

stdout
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

  1. Use dataclasses with `frozen=True` for value objects to enforce immutability
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.