DDD IMPLEMENTATION PATTERNS
Beyond strategic and tactical design theory, Domain-Driven Design succeeds through proven implementation patterns. These are concrete, reusable solutions to architectural challenges that emerge when building domain-driven systems. This guide covers the essential patterns that enable teams to translate DDD principles into robust, maintainable code that scales with your business.
Implementation patterns bridge the gap between abstract domain concepts and practical code structures. They provide proven approaches to common architectural problems: managing persistence, coordinating behavior across aggregates, maintaining consistency, and communicating domain state changes. Each pattern addresses a specific challenge while preserving your domain model's integrity and expressive power.
THE REPOSITORY PATTERN
The Repository Pattern is fundamental to DDD. It abstracts data access logic and creates an in-memory collection-like interface for aggregates. Rather than spreading database queries throughout your code, repositories provide a single, controlled gateway for loading and persisting aggregates.
Key responsibilities:
- Query for aggregates by identity or criteria
- Persist new aggregates to storage
- Update existing aggregates in storage
- Remove aggregates when no longer needed
- Abstract the underlying persistence mechanism (SQL, NoSQL, file-based)
A well-designed repository keeps your domain objects free from persistence concerns. Your Customer aggregate doesn't know whether it's stored in PostgreSQL, MongoDB, or memory. This separation enables easier testing, cleaner code, and the flexibility to change storage mechanisms without rewriting domain logic.
Repository interface example: A Customer repository might expose methods like findById(customerId), findByEmail(email), add(customer), and remove(customerId). The implementation details of how customers are actually fetched from the database remain hidden from domain code.
THE UNIT OF WORK PATTERN
The Unit of Work Pattern manages object state and coordinates the persistence of multiple aggregates as a single atomic operation. When business logic modifies several aggregates that must be persisted together, the Unit of Work ensures all-or-nothing consistency.
Core responsibilities:
- Track changes to aggregates within a business operation
- Batch multiple aggregate changes together
- Commit all changes as a single database transaction
- Rollback all changes if any part of the operation fails
- Maintain an identity map to prevent duplicate loads of the same aggregate
In practice, the Unit of Work is often implemented as part of your object-relational mapping framework or as an explicit service managing a transaction scope. For a banking system transferring money between accounts, the Unit of Work ensures that if the debit succeeds but the credit fails, both changes roll back together, maintaining account balance integrity.
THE AGGREGATE PATTERN IN PRACTICE
The Aggregate Pattern groups related entities and value objects into cohesive units. An aggregate has a root entity, internal consistency rules, and clear boundaries. Only the root can be referenced externally; internal entities are accessed only through the root.
Aggregate design principles:
- Keep aggregates small and focused on a single business concept
- Enforce invariants at the aggregate boundary
- Reference other aggregates only by identity, never by holding direct references
- Design operations to maintain consistency without requiring external coordination
- Use repositories to load and persist entire aggregates as units
An Order aggregate, for example, includes OrderLineItems, ShippingAddress, and PaymentInfo. All modifications to the order go through the Order root entity. Business rules like "line item quantities cannot exceed inventory" are enforced at the aggregate boundary. When external code needs order information, it always goes through the Order aggregate, never directly accessing line items.
VALUE OBJECTS FOR DOMAIN CLARITY
Value objects are immutable objects identified by their values rather than identity. They represent domain concepts like Money, Address, EmailAddress, or UserId. Unlike entities, value objects are compared by their attributes, and multiple instances with identical values are interchangeable.
Value object benefits:
- Express domain concepts explicitly in code
- Encapsulate validation logic within the value object
- Enable safe sharing across aggregates (immutability guarantees)
- Improve code clarity through domain-aligned types
- Reduce bugs by making invalid states impossible to represent
Using a Money value object instead of a raw decimal enforces that amounts are never negative and currency is always specified. Using an EmailAddress value object validates format at construction. Repositories and aggregates can freely share value objects knowing they cannot be unexpectedly modified, eliminating whole categories of concurrency bugs.
DOMAIN EVENTS AND EVENT PUBLISHING
Domain events are immutable records of something important that happened in your domain. When an aggregate executes business logic, it may emit domain events signaling that important state changes occurred. Other parts of the system subscribe to these events and react accordingly.
Domain events support:
- Decoupling aggregates and bounded contexts
- Enabling side effects without tight coupling
- Creating an audit trail of business events
- Supporting event sourcing architectures
- Coordinating across microservices asynchronously
When an Order aggregate accepts payment, it might emit an OrderPaid event. Warehousing systems subscribe to OrderPaid events and prepare shipments. Accounting systems post revenue. Notification systems send confirmation emails. All systems react independently through event subscribers, maintaining loose coupling. If a new requirement emerges, you add a new event subscriber without modifying the order processing code.
DEPENDENCY INJECTION FOR DOMAIN CODE
Dependency Injection (DI) decouples domain objects from their dependencies, making code testable and flexible. Rather than having a Product aggregate create its own discount calculator, the calculator is injected, allowing different implementations in different contexts.
DI patterns in DDD:
- Inject repositories into domain services and command handlers
- Inject value objects and domain services into aggregates
- Inject event publishers for domain event distribution
- Use constructor injection to make dependencies explicit
- Keep application services thin; business logic stays in domain objects
A CreateOrder command handler receives injected repositories for customers, products, and orders, along with a PricingService. It validates business rules using these dependencies, creates the Order aggregate, publishes domain events, and persists via repository. Testing requires only mocking these dependencies, not complex infrastructure setup.
SPECIFICATION PATTERN FOR COMPLEX QUERIES
The Specification Pattern encapsulates complex query logic into reusable, composable objects. Instead of writing ad-hoc SQL in multiple places, specifications define business-meaningful queries that can be tested independently.
Specification usage:
- Express complex business queries in code
- Reuse query logic across application services
- Make complex criteria explicit and testable
- Support different persistence mechanisms through a common interface
- Combine specifications for complex conditions
A specification might express "find all overdue, unpaid invoices for premium customers in the technology sector." Instead of inline queries scattered throughout the codebase, this becomes an OverdueUnpaidPremiumInvoicesSpec that's versioned, tested, and reusable. Repositories know how to evaluate specifications against their storage mechanism.
COMMAND QUERY RESPONSIBILITY SEGREGATION
Command Query Responsibility Segregation (CQRS) separates operations that modify state from operations that read state. Commands represent requests that change the domain; queries retrieve information. This separation enables independent optimization and scaling of read and write paths.
CQRS benefits in DDD:
- Scale reads and writes independently
- Use different storage mechanisms for commands and queries
- Implement complex, optimized read models without domain logic constraints
- Create clear command and query abstractions in your domain
- Enable event sourcing by storing commands as events
A typical CQRS setup in DDD implements commands that modify aggregates (CreateCustomer, PlaceOrder, CancelOrder) and maintains separate read models optimized for queries (CustomerList, OrderSearch, InvoiceReport). The command side enforces domain invariants; the query side optimizes for user experience. They stay synchronized through domain events or event streams.
DOMAIN SERVICE ORCHESTRATION
Domain services coordinate logic that doesn't naturally belong to any single aggregate. A domain service receives multiple aggregates and domain objects as dependencies, orchestrates their interaction, and ensures business rules are maintained across aggregate boundaries.
Domain service patterns:
- Accept aggregates and domain objects as parameters
- Coordinate operations involving multiple aggregates
- Maintain transactional consistency across multiple roots
- Publish domain events reflecting cross-aggregate changes
- Remain testable by injecting aggregate dependencies
A MoneyTransferService receives a from-account aggregate and a to-account aggregate, validates that the transfer is legal, debits one account, credits the other, and publishes a MoneyTransferred event. Neither Account aggregate needed to know about the other; the service orchestrated their collaboration while maintaining domain invariants across both accounts.
PERSISTENCE TESTING WITHOUT DATABASES
Separating domain logic from persistence concerns enables fast, focused unit tests. Tests verify that aggregates enforce business rules correctly without requiring database setup. Integration tests then verify that repositories correctly persist and load aggregates.
Testing strategy:
- Unit test aggregates with pure domain logic, no persistence dependencies
- Integration test repositories against a test database
- Mock repositories when testing domain services
- Use in-memory repositories for most application service tests
- Run expensive end-to-end tests only for critical paths
A test verifying that an Order rejects invalid line items runs in milliseconds with no database. A test verifying that OrderRepository correctly reconstructs an Order with all relationships intact runs against a test database. A test verifying that CreateOrderHandler enforces business rules runs with mocked repositories. This layered testing approach maintains speed while covering all critical behaviors.
ANTI-PATTERNS TO AVOID
Anemic Aggregates: Aggregates reduced to data containers with getters/setters, all logic moved to services. This loses DDD's benefits; domain rules scatter across the codebase.
God Objects: Aggregates that grow to include too much logic and too many responsibilities. Keep aggregates focused and small.
Leaky Abstractions: Repositories that expose database-specific query languages. Abstractions should hide persistence details completely.
Transaction Scripts: Returning to procedural scripts instead of domain-driven design when things get complex. This negates DDD's advantage.
Event Sourcing Overuse: Applying event sourcing everywhere as a general persistence mechanism. It's powerful but adds complexity; use only where the benefits justify the costs.
Mastering these implementation patterns transforms DDD from theoretical framework into practical architecture. They provide proven solutions to the real challenges that emerge when building domain-driven systems at scale.