Domain-Driven Design (DDD)

Domain-Driven Design is a software development approach that focuses on creating software that accurately reflects the real-world business domain it serves. Rather than starting with technical concerns, DDD emphasizes understanding the business problem first, then modeling the software around that understanding.

Core Concepts

The Domain

The domain represents the sphere of knowledge and activity around which the business application revolves. It encompasses the business rules, processes, and terminology that define how the organization operates.

Ubiquitous Language

This is a common vocabulary shared between developers and domain experts. Every term used in the code should have the same meaning when business stakeholders discuss the system. This eliminates translation errors between business requirements and technical implementation.

Bounded Context

A bounded context defines the boundaries within which a particular domain model applies. Different parts of a large system may have different models for the same concept, and that’s acceptable as long as each model is consistent within its own boundary.

Key Building Blocks

Entities

Objects that have a distinct identity that runs through time and different states. Two entities are considered the same if they have the same identity, regardless of their other attributes.

Value Objects

Objects that are defined by their attributes rather than their identity. Two value objects are equal if all their attributes are equal. They are immutable and have no identity.

Aggregates

A cluster of associated objects that are treated as a unit for data changes. Each aggregate has a root entity that controls access to the aggregate’s members.

Domain Services

When an operation doesn’t naturally fit within an entity or value object, it belongs in a domain service. These encapsulate business logic that spans multiple objects.

Repositories

Provide an abstraction for accessing domain objects, acting as an in-memory collection of objects. They encapsulate the logic needed to obtain object references.

Architecture Flow

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  Presentation   │────│   Application   │────│     Domain      │
│     Layer       │    │     Layer       │    │     Layer       │
│                 │    │                 │    │                 │
│ • Controllers   │    │ • Use Cases     │    │ • Entities      │
│ • Views         │    │ • Commands      │    │ • Value Objects │
│ • DTOs          │    │ • Queries       │    │ • Services      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                │                        │
                                │                        │
                                ▼                        ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│ Infrastructure  │    │   Application   │    │     Domain      │
│     Layer       │────│   Services      │────│   Repositories  │
│                 │    │                 │    │  (Interfaces)   │
│ • Database      │    │ • Email         │    │                 │
│ • External APIs │    │ • Logging       │    │                 │
│ • File System   │    │ • Validation    │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Benefits of Domain-Driven Design

Business Alignment: The software model closely mirrors the business model, making it easier for non-technical stakeholders to understand and validate the system.

Maintainability: By organizing code around business concepts rather than technical layers, the system becomes more intuitive to navigate and modify.

Flexibility: Well-defined boundaries and abstractions make it easier to change implementations without affecting the core business logic.

Communication: The ubiquitous language improves communication between technical and non-technical team members.

When to Use DDD

Domain-Driven Design is most beneficial for:

  • Complex business domains with intricate rules and processes
  • Large systems that will evolve over time
  • Projects where close collaboration with domain experts is possible
  • Applications where the business logic is the primary source of complexity

DDD may be overkill for simple CRUD applications or systems where the technical complexity far outweighs the business complexity.

Common Patterns in DDD

Repository Pattern

Encapsulates the logic needed to access data sources, centralizing common data access functionality and providing better maintainability.

Factory Pattern

Creates complex objects and aggregates, ensuring they are properly initialized and all business rules are satisfied during creation.

Specification Pattern

Encapsulates business rules in a way that can be combined and reused, making complex queries and validations more maintainable.

Domain-Driven Design provides a structured approach to building software that truly serves the business needs while maintaining technical excellence. The key is to start with understanding the domain thoroughly before diving into implementation details.

Example 1 Explanation: E-commerce Order Domain

This example demonstrates core DDD concepts through an order management system:

from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
import uuid

# Value Objects - Immutable objects defined by their attributes
@dataclass(frozen=True)
class Money:
    """Represents monetary value with currency"""
    amount: Decimal
    currency: str = "USD"
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)

@dataclass(frozen=True)
class Address:
    """Customer address value object"""
    street: str
    city: str
    postal_code: str
    country: str

# Entities - Objects with identity that persists over time
class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class OrderItem:
    """Represents an item within an order"""
    def __init__(self, product_id: str, quantity: int, unit_price: Money):
        self.product_id = product_id
        self.quantity = quantity
        self.unit_price = unit_price
    
    def total_price(self) -> Money:
        """Calculate total price for this item"""
        return Money(
            self.unit_price.amount * self.quantity, 
            self.unit_price.currency
        )

class Order:
    """Order aggregate root - controls access to order data"""
    def __init__(self, customer_id: str, shipping_address: Address):
        self.id = str(uuid.uuid4())  # Unique identity
        self.customer_id = customer_id
        self.shipping_address = shipping_address
        self.items: List[OrderItem] = []
        self.status = OrderStatus.PENDING
    
    def add_item(self, product_id: str, quantity: int, unit_price: Money) -> None:
        """Add item to order - business rule enforcement"""
        if self.status != OrderStatus.PENDING:
            raise ValueError("Cannot modify confirmed order")
        
        # Check if item already exists and update quantity
        for item in self.items:
            if item.product_id == product_id:
                item.quantity += quantity
                return
        
        # Add new item
        self.items.append(OrderItem(product_id, quantity, unit_price))
    
    def confirm_order(self) -> None:
        """Confirm the order - business rule enforcement"""
        if not self.items:
            raise ValueError("Cannot confirm empty order")
        if self.status != OrderStatus.PENDING:
            raise ValueError("Order already processed")
        
        self.status = OrderStatus.CONFIRMED
    
    def total_amount(self) -> Money:
        """Calculate total order amount"""
        if not self.items:
            return Money(Decimal('0'))
        
        total = self.items[0].total_price()
        for item in self.items[1:]:
            total = total.add(item.total_price())
        return total

# Repository Interface - Abstract data access
class OrderRepository(ABC):
    """Repository interface for order persistence"""
    
    @abstractmethod
    def save(self, order: Order) -> None:
        """Save order to storage"""
        pass
    
    @abstractmethod
    def find_by_id(self, order_id: str) -> Optional[Order]:
        """Find order by ID"""
        pass
    
    @abstractmethod
    def find_by_customer(self, customer_id: str) -> List[Order]:
        """Find all orders for a customer"""
        pass

# Domain Service - Business logic that doesn't fit in entities
class OrderService:
    """Domain service for complex order operations"""
    
    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository
    
    def create_order(self, customer_id: str, shipping_address: Address, 
                    items: List[tuple[str, int, Money]]) -> Order:
        """Create a new order with validation"""
        if not items:
            raise ValueError("Order must contain at least one item")
        
        order = Order(customer_id, shipping_address)
        
        # Add all items
        for product_id, quantity, unit_price in items:
            if quantity <= 0:
                raise ValueError("Item quantity must be positive")
            order.add_item(product_id, quantity, unit_price)
        
        return order
    
    def process_order(self, order_id: str) -> bool:
        """Process order through business workflow"""
        order = self.order_repository.find_by_id(order_id)
        if not order:
            return False
        
        # Business logic for order processing
        if order.status == OrderStatus.PENDING:
            order.confirm_order()
            self.order_repository.save(order)
            return True
        
        return False

# Example Usage
if __name__ == "__main__":
    # Create value objects
    address = Address("123 Main St", "New York", "10001", "USA")
    item_price = Money(Decimal('29.99'))
    
    # Create domain service (would inject real repository in production)
    class InMemoryOrderRepository(OrderRepository):
        def __init__(self):
            self.orders = {}
        
        def save(self, order: Order) -> None:
            self.orders[order.id] = order
        
        def find_by_id(self, order_id: str) -> Optional[Order]:
            return self.orders.get(order_id)
        
        def find_by_customer(self, customer_id: str) -> List[Order]:
            return [o for o in self.orders.values() if o.customer_id == customer_id]
    
    repository = InMemoryOrderRepository()
    order_service = OrderService(repository)
    
    # Create and process order
    items = [("PROD-001", 2, item_price), ("PROD-002", 1, Money(Decimal('19.99')))]
    order = order_service.create_order("CUST-123", address, items)
    
    print(f"Order ID: {order.id}")
    print(f"Total: {order.total_amount().amount} {order.total_amount().currency}")
    print(f"Status: {order.status.value}")
    
    # Save and process order
    repository.save(order)
    success = order_service.process_order(order.id)
    print(f"Order processed: {success}")
    print(f"New status: {order.status.value}")

Value Objects: Money and Address are immutable objects defined by their attributes. Two Money objects with the same amount and currency are considered equal.

Entity: Order has a unique identity (UUID) and maintains state over time. The identity remains constant even as the order’s attributes change.

Aggregate Root: Order serves as the aggregate root, controlling access to OrderItem objects and enforcing business rules like preventing modifications to confirmed orders.

Repository Pattern: OrderRepository provides an abstraction for data access, allowing the domain to remain independent of persistence concerns.

Domain Service: OrderService handles complex business operations that involve multiple entities or don’t naturally belong to a single object.

The code enforces business rules at the domain level, such as preventing empty orders from being confirmed and ensuring item quantities are positive.

Example 2 Explanation: Library Book Management Domain

This example showcases DDD principles in a library context:

from abc import ABC, abstractmethod
from typing import Optional, List
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
import uuid

# Value Objects
@dataclass(frozen=True)
class ISBN:
    """International Standard Book Number value object"""
    value: str
    
    def __post_init__(self):
        # Simple ISBN validation (real implementation would be more thorough)
        if len(self.value.replace('-', '')) not in [10, 13]:
            raise ValueError("Invalid ISBN format")

@dataclass(frozen=True)
class MembershipId:
    """Library membership identifier"""
    value: str

# Entities
class BookStatus(Enum):
    AVAILABLE = "available"
    BORROWED = "borrowed"
    RESERVED = "reserved"
    MAINTENANCE = "maintenance"

class Book:
    """Book entity with unique copy identity"""
    def __init__(self, isbn: ISBN, title: str, author: str, copy_number: int):
        self.id = str(uuid.uuid4())  # Unique copy identifier
        self.isbn = isbn
        self.title = title
        self.author = author
        self.copy_number = copy_number
        self.status = BookStatus.AVAILABLE
        self.current_borrower: Optional[MembershipId] = None
        self.due_date: Optional[datetime] = None
    
    def borrow_to(self, member_id: MembershipId, loan_period_days: int = 14) -> None:
        """Lend book to a member"""
        if self.status != BookStatus.AVAILABLE:
            raise ValueError(f"Book is not available (status: {self.status.value})")
        
        self.status = BookStatus.BORROWED
        self.current_borrower = member_id
        self.due_date = datetime.now() + timedelta(days=loan_period_days)
    
    def return_book(self) -> None:
        """Return the book to library"""
        if self.status != BookStatus.BORROWED:
            raise ValueError("Book is not currently borrowed")
        
        self.status = BookStatus.AVAILABLE
        self.current_borrower = None
        self.due_date = None
    
    def is_overdue(self) -> bool:
        """Check if book is overdue"""
        if self.status != BookStatus.BORROWED or not self.due_date:
            return False
        return datetime.now() > self.due_date

class Member:
    """Library member entity"""
    def __init__(self, membership_id: MembershipId, name: str, email: str):
        self.membership_id = membership_id
        self.name = name
        self.email = email
        self.borrowed_books: List[str] = []  # Book IDs
        self.is_active = True
    
    def can_borrow_book(self, max_books: int = 5) -> bool:
        """Check if member can borrow more books"""
        return self.is_active and len(self.borrowed_books) < max_books
    
    def borrow_book(self, book_id: str) -> None:
        """Add book to member's borrowed list"""
        if book_id not in self.borrowed_books:
            self.borrowed_books.append(book_id)
    
    def return_book(self, book_id: str) -> None:
        """Remove book from member's borrowed list"""
        if book_id in self.borrowed_books:
            self.borrowed_books.remove(book_id)

# Repository Interfaces
class BookRepository(ABC):
    """Repository for book persistence"""
    
    @abstractmethod
    def save(self, book: Book) -> None:
        pass
    
    @abstractmethod
    def find_by_id(self, book_id: str) -> Optional[Book]:
        pass
    
    @abstractmethod
    def find_by_isbn(self, isbn: ISBN) -> List[Book]:
        pass
    
    @abstractmethod
    def find_available_books(self) -> List[Book]:
        pass

class MemberRepository(ABC):
    """Repository for member persistence"""
    
    @abstractmethod
    def save(self, member: Member) -> None:
        pass
    
    @abstractmethod
    def find_by_id(self, membership_id: MembershipId) -> Optional[Member]:
        pass

# Domain Service
class LibraryService:
    """Domain service for library operations"""
    
    def __init__(self, book_repo: BookRepository, member_repo: MemberRepository):
        self.book_repo = book_repo
        self.member_repo = member_repo
    
    def borrow_book(self, book_id: str, membership_id: MembershipId) -> bool:
        """Complete book borrowing process"""
        # Retrieve entities
        book = self.book_repo.find_by_id(book_id)
        member = self.member_repo.find_by_id(membership_id)
        
        if not book or not member:
            return False
        
        # Enforce business rules
        if not member.can_borrow_book():
            raise ValueError("Member cannot borrow more books")
        
        if book.status != BookStatus.AVAILABLE:
            raise ValueError("Book is not available for borrowing")
        
        # Execute domain operations
        book.borrow_to(membership_id)
        member.borrow_book(book_id)
        
        # Persist changes
        self.book_repo.save(book)
        self.member_repo.save(member)
        
        return True
    
    def return_book(self, book_id: str) -> bool:
        """Complete book return process"""
        book = self.book_repo.find_by_id(book_id)
        if not book or book.status != BookStatus.BORROWED:
            return False
        
        # Get the borrower
        borrower = self.member_repo.find_by_id(book.current_borrower)
        if borrower:
            borrower.return_book(book_id)
            self.member_repo.save(borrower)
        
        # Return the book
        book.return_book()
        self.book_repo.save(book)
        
        return True
    
    def find_overdue_books(self) -> List[Book]:
        """Find all overdue books - complex query operation"""
        all_books = self.book_repo.find_available_books()  # This would be all books in real implementation
        return [book for book in all_books if book.is_overdue()]

# Example Usage
if __name__ == "__main__":
    # In-memory implementations for demonstration
    class InMemoryBookRepository(BookRepository):
        def __init__(self):
            self.books = {}
        
        def save(self, book: Book) -> None:
            self.books[book.id] = book
        
        def find_by_id(self, book_id: str) -> Optional[Book]:
            return self.books.get(book_id)
        
        def find_by_isbn(self, isbn: ISBN) -> List[Book]:
            return [b for b in self.books.values() if b.isbn == isbn]
        
        def find_available_books(self) -> List[Book]:
            return [b for b in self.books.values() if b.status == BookStatus.AVAILABLE]
    
    class InMemoryMemberRepository(MemberRepository):
        def __init__(self):
            self.members = {}
        
        def save(self, member: Member) -> None:
            self.members[member.membership_id.value] = member
        
        def find_by_id(self, membership_id: MembershipId) -> Optional[Member]:
            return self.members.get(membership_id.value)
    
    # Create repositories and service
    book_repo = InMemoryBookRepository()
    member_repo = InMemoryMemberRepository()
    library_service = LibraryService(book_repo, member_repo)
    
    # Create domain objects
    isbn = ISBN("978-0-123456-78-9")
    book = Book(isbn, "Domain-Driven Design", "Eric Evans", 1)
    member_id = MembershipId("MEM-001")
    member = Member(member_id, "John Doe", "john@example.com")
    
    # Save to repositories
    book_repo.save(book)
    member_repo.save(member)
    
    print(f"Book: {book.title} (Status: {book.status.value})")
    print(f"Member: {member.name} (Books borrowed: {len(member.borrowed_books)})")
    
    # Borrow book
    success = library_service.borrow_book(book.id, member_id)
    print(f"\nBorrow successful: {success}")
    print(f"Book status: {book.status.value}")
    print(f"Due date: {book.due_date.strftime('%Y-%m-%d') if book.due_date else 'None'}")
    print(f"Member books: {len(member.borrowed_books)}")
    
    # Return book
    success = library_service.return_book(book.id)
    print(f"\nReturn successful: {success}")
    print(f"Book status: {book.status.value}")
    print(f"Member books: {len(member.borrowed_books)}")

Value Objects: ISBN and MembershipId are immutable identifiers with built-in validation. They ensure data integrity at the type level.

Entities: Both Book and Member have unique identities. A Book represents a specific physical copy (not just a title), while Member represents an individual library user.

Business Rules Enforcement: The domain objects enforce rules like preventing borrowing when a member has reached their limit or when a book isn’t available.

Domain Service: LibraryService orchestrates complex operations that involve multiple entities and repositories, such as the complete borrowing process that updates both book and member states.

Repository Pattern: Separate repositories handle persistence concerns for different aggregate roots, keeping the domain logic clean and testable.

The design maintains clear separation between business logic (in entities and services) and infrastructure concerns (in repositories), making the system easier to understand and modify as business requirements change.

Track your progress

Mark this subtopic as completed when you finish reading.