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.