DDD Tactical Patterns#
Problem statement (interviewer prompt)
Model the domain of an e-commerce order: entities (Order, OrderLine), value objects (Money, Address), aggregate roots, domain events (OrderPlaced, OrderShipped), and bounded contexts (Billing vs Shipping vs Inventory). Show the class diagram + state machine.
classDiagram
class Aggregate {
<<root>>
+id
+invariants enforced
}
class Entity {
+id
+mutable, lifecycle
}
class ValueObject {
+immutable
+equality by value
}
class DomainEvent {
+past-tense fact
}
Aggregate o--> Entity
Aggregate o--> ValueObject
Aggregate ..> DomainEvent
DDD tactical building blocks model a single bounded context: aggregates enforce invariants, entities have identity and lifecycle, value objects are immutable values, domain events record what happened.
The four core types#
classDiagram
class Order {
<<aggregate root>>
-id: OrderId
-customerId: CustomerId
-lines: List~OrderLine~
-status: OrderStatus
-total: Money
+addLine(item, qty, price)
+pay()
+cancel()
}
class OrderLine {
<<entity>>
-lineId
-item: ItemId
-qty: int
-unitPrice: Money
}
class Money {
<<value object>>
+amount
+currency
}
class OrderPlaced {
<<domain event>>
+orderId
+customerId
+total
+occurredAt
}
Order o--> OrderLine
OrderLine --> Money
Order ..> OrderPlaced
Aggregate#
A cluster of objects treated as one consistency boundary.
- One aggregate root (here
Order) is the only entry point. - External code references the root only by id.
- All invariants must be valid after every public method.
- One DB transaction per aggregate change.
class Order {
void addLine(ItemId item, int qty, Money unitPrice) {
if (status != DRAFT) throw new InvalidStateException();
if (qty <= 0) throw new IllegalArgumentException();
lines.add(new OrderLine(item, qty, unitPrice));
recompute(); // invariant: total = sum(lines)
}
}
Entity#
Has identity that persists through state changes. Two entities with the same id are equal, regardless of attributes.
Order(root) andOrderLine(inside the aggregate) are entities.- Equality by id, not by value.
Value Object#
Immutable, identified by attributes. Two Money(10, "USD") are interchangeable.
record Money(BigDecimal amount, String currency) {
Money add(Money other) {
if (!currency.equals(other.currency)) throw new CurrencyMismatch();
return new Money(amount.add(other.amount), currency);
}
}
Why prefer value objects: - No accidental aliasing. - Self-validating (constructor enforces invariants). - Side-effect-free operations. - Free thread-safety.
Domain Event#
A past-tense fact the domain emits when something significant happens.
class OrderPlaced {
final OrderId orderId;
final CustomerId customerId;
final Money total;
final Instant occurredAt;
}
- Emitted from the aggregate root.
- Dispatched after the UoW commits (or via outbox).
- Decoupled from external side-effects (email, inventory, analytics).
Domain Service#
Stateless operation that doesn't naturally belong on a single aggregate.
Use sparingly - most logic belongs on the entity / aggregate, not in services.
Bounded Context#
The boundary in which a model has one meaning. "Customer" in the Billing context is not the same shape as "Customer" in Shipping. Cross-context links are anti-corruption layers, not shared classes.
flowchart LR
subgraph Billing
B[Customer billing model]
end
subgraph Shipping
S[Customer shipping model]
end
subgraph Marketing
M[Customer marketing model]
end
Billing -.events.-> Shipping
Billing -.events.-> Marketing
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class B,S,M client;
Repository (for aggregates)#
One repository per aggregate root. See repository-pattern.
Putting it together: a use case#
class PlaceOrder {
PlaceOrder(OrderRepository orders, EventBus events) { ... }
Result place(PlaceOrderCmd cmd) {
var order = new Order(cmd.customerId());
for (var line : cmd.lines()) order.addLine(line.item, line.qty, line.price);
orders.save(order);
events.publish(new OrderPlaced(order.id(), ...));
return Result.ok(order.id());
}
}
Only the aggregate enforces business rules. The use case is a thin coordinator.
Anti-patterns#
- Anaemic domain - entities are bags of getters/setters with no behaviour.
- Aggregates referencing other aggregates by object - should be by id only.
- Cross-aggregate transactions - use sagas instead.
- Generic "BaseEntity" with everything in it - over-abstracted.
- "Customer" used across every context - bounded contexts exist for a reason.
Where this shows up#
- Every LLD-flavoured problem (Parking Lot, Hotel, Library, ATM) naturally maps to aggregates.
- Banking ledger, payment gateway - aggregates with strong invariants.
- E-commerce orders, bookings.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Observability | metrics, logs, traces, SLOs | observability |
HLD |
Event sourcing + CQRS | commands -> events; separate read model | event-sourcing-cqrs |
LLD |
DDD tactical | entity / value object / aggregate / event | ddd-tactical |
LLD |
Immutability | immutable types, persistent collections | immutability |
LLD |
Error handling | exceptions vs Result, error boundaries | error-handling |
Quick reference#
Sizing aggregates#
- Small. If
save()writes 100 rows, the aggregate is too big. - "Cluster around the invariant" - what must be transactionally consistent?
- Cross-aggregate references → ids only, eventual consistency via events.
Naming#
- Aggregate root usually = the noun (
Order,Account,Booking). - Value objects = self-describing types (
Money,Email,DateRange). - Domain events = past tense (
OrderPlaced, notPlaceOrder).
Eventual consistency#
- "When customer is renamed, update all their orders" - emit
CustomerRenamed, listener updates each order aggregate eventually. - Surface staleness window to the product team; SLAs in seconds, not microseconds.
Refs#
- Eric Evans: Domain-Driven Design (the original, 2003).
- Vaughn Vernon: Implementing Domain-Driven Design (2013).
- Vaughn Vernon: Domain-Driven Design Distilled (short version).
- DDD Crew: dddcrew.com (modern resources + bounded-context canvas).
FAQ#
What is the difference between an entity and a value object in DDD?#
An entity has a unique identity that persists over time, while a value object is defined by its attributes and is immutable. Money and Address are value objects; Order and User are entities.
What is an aggregate root in domain-driven design?#
An aggregate root is the single entity that owns a cluster of related objects and enforces invariants for that cluster. All external access to the aggregate goes through the root.
When should I emit a domain event?#
Emit a domain event whenever a state change matters to other parts of the system, like OrderPlaced or PaymentCaptured. Events are past-tense facts the aggregate publishes after committing.
Are DDD tactical patterns only for microservices?#
No. Tactical patterns shape how you model one bounded context regardless of deployment. They work equally well in a modular monolith or a single service.
How are aggregates persisted to a database?#
A repository loads and saves whole aggregates as a unit, often inside a transaction. Each aggregate maps to one or several tables, and you never modify a child entity outside its root.
Related Topics#
- Clean Architecture: the architectural home for DDD aggregates, services, and ports
- Repository Pattern: the standard persistence abstraction for DDD aggregates
- Event Sourcing and CQRS: event-driven persistence model that pairs naturally with DDD domain events
Further reading#
Curated, high-credibility sources for going deeper on this topic.