Implementing Domain-Driven Design Patterns
1. Bounded Context Pattern
| Aspect | Detail |
|---|---|
| Definition | Explicit boundary inside which a domain model is consistent |
| Scope | Single ubiquitous language; one team usually |
| Maps To | Typically one microservice (or small cluster) |
| Cross-Context | Translate via Anti-Corruption Layer or Published Language |
Example: Same Concept, Different Contexts
// Sales Context: Customer = lead with revenue potential
class Customer { String id; BigDecimal lifetimeValue; List<Opportunity> opps; }
// Support Context: Customer = ticket holder
class Customer { String id; String tier; List<Ticket> openTickets; }
// Billing Context: Customer = payer
class Customer { String id; PaymentMethod method; BigDecimal balance; }
2. Aggregate Pattern
| Rule | Description |
|---|---|
| Root Entity | Single entry point; external code references only the root |
| Consistency Boundary | One transaction modifies one aggregate |
| Reference by ID | Other aggregates referenced by ID, not direct object |
| Small Aggregates | Prefer small to reduce contention |
| Invariants | Root enforces business invariants on its members |
Example: Order Aggregate (Java)
public class Order { // Aggregate Root
private final OrderId id;
private final CustomerId customerId; // Reference by ID
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status;
public void addLine(ProductId productId, int qty, Money price) {
if (status != DRAFT) throw new IllegalStateException("Order locked");
lines.add(new OrderLine(productId, qty, price));
}
public Money total() { return lines.stream().map(OrderLine::subtotal).reduce(Money.ZERO, Money::add); }
}
3. Entity Pattern
| Property | Description |
|---|---|
| Identity | Has stable unique ID over lifetime |
| Mutable | State can change; identity does not |
| Equality | By ID, not by attributes |
| Examples | Order, Customer, Account |
4. Value Object Pattern
| Property | Description |
|---|---|
| No Identity | Defined entirely by its attributes |
| Immutable | Once created, never modified |
| Equality | By value (all fields equal) |
| Examples | Money, Address, DateRange, Coordinates |
Example: Money Value Object (Java 21+ record)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount == null || currency == null) throw new IllegalArgumentException();
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalStateException("Mismatch");
return new Money(amount.add(other.amount), currency);
}
}
5. Repository Pattern
| Aspect | Detail |
|---|---|
| Purpose | Abstracts persistence; collection-like interface for aggregates |
| Returns | Aggregate roots only |
| Implementation | One repo per aggregate root |
| Common Methods | findById, save, delete, findBy* |
Example: Repository Interface
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId customerId);
void save(Order order);
void delete(OrderId id);
}
6. Domain Event Pattern
| Property | Detail |
|---|---|
| Naming | Past tense (OrderPlaced, PaymentCaptured) |
| Immutability | Events are facts; never modified |
| Carries | Aggregate ID, occurred-at timestamp, relevant data |
| Publishing | Via Outbox after aggregate save |
Example: Domain Event
public record OrderPlaced(
OrderId orderId,
CustomerId customerId,
Money total,
Instant occurredAt
) implements DomainEvent {}
7. Domain Service Pattern
| When to Use | Why |
|---|---|
| Operation spans multiple aggregates | Doesn't naturally belong to one |
| External resource needed | e.g., currency converter, tax calculator |
| Stateless logic | Pure domain rule, no own state |
Warning: Don't use domain services as a dumping ground. Most behavior should live on entities/aggregates.
8. Factory Pattern
| Aspect | Detail |
|---|---|
| Purpose | Encapsulate complex aggregate creation |
| Use When | Construction has invariants/side effects beyond constructor |
| Forms | Static factory method, factory class, abstract factory |
Example: Order Factory
public class OrderFactory {
public Order createFromCart(Cart cart, CustomerId customerId) {
Order order = new Order(OrderId.generate(), customerId);
cart.items().forEach(i -> order.addLine(i.productId(), i.qty(), i.price()));
order.applyDiscounts(discountPolicy.eligible(customerId));
return order;
}
}
9. Specification Pattern
| Aspect | Detail |
|---|---|
| Purpose | Encapsulate business rule as composable predicate |
| Operations | and, or, not for combining |
| Uses | Validation, query criteria, selection rules |
Example: Specification
public interface Specification<T> { boolean isSatisfiedBy(T t); }
Specification<Order> eligibleForFreeShipping = o ->
o.total().amount().compareTo(new BigDecimal("50")) >= 0
&& o.shippingAddress().country().equals("US");
10. Ubiquitous Language Pattern
| Principle | Detail |
|---|---|
| Shared Vocabulary | Same terms used by domain experts, devs, code, tests |
| Per Bounded Context | Each context has its own dictionary |
| Refactor Together | If language changes, code/docs change too |
| Tests as Spec | Test names mirror business statements |
11. Context Mapping Pattern
| Relationship | Description |
|---|---|
| Partnership | Two contexts coordinate releases |
| Shared Kernel | Small shared model both depend on (use sparingly) |
| Customer/Supplier | Upstream serves downstream priorities |
| Conformist | Downstream conforms to upstream model as-is |
| Anti-Corruption Layer | Downstream translates from upstream |
| Open Host Service | Upstream exposes published protocol for many consumers |
| Published Language | Well-documented shared format (e.g., schema) |
| Separate Ways | No integration; contexts independent |
| Big Ball of Mud | Recognize chaotic legacy; isolate via ACL |