AI/TLDRai-tldr.dev · every AI release as it ships - models · tools · repos · benchmarksPOMEGRApomegra.io · AI stock market analysis - autonomous investment agents

DOMAIN-DRIVEN DESIGN

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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.