Pragmatic DDD: Bounded Contexts and Aggregates Under High Traffic
Pragmatic Domain-Driven Design: bounded context boundaries, the lock and scale impact of aggregate design, ubiquitous language and their counterparts in high-traffic systems.
What does DDD have to do with high traffic?
Everything: what scales a system under high traffic is correctly drawn boundaries — and DDD is the discipline of drawing boundaries. Bounded contexts decide what scales independently, aggregates decide what locks in one transaction, domain events decide what flows asynchronously. This article treats DDD not as ceremony but as a scalability instrument — not every pattern in the book, but the core that pays off in the field.
Bounded context: the unit of scaling
A bounded context is the boundary within which one business language and model holds: in Catalog a 'product' is rich content, in Cart it is price+quantity, in Shipping it is weight+dimensions. The scaling counterpart is concrete: each context is an independently scalable unit — on campaign day you scale Cart to 20 replicas and keep Catalog at 4 behind a cache. Boundary violations are equally concrete: two contexts writing the same table, one querying another's internal model — the leaks that produce a 'distributed monolith'. The context map (which context talks to which, in what relationship: customer-supplier, conformist, anticorruption layer) is the architecture's real diagram; ideally, team structure mirrors it.
Aggregate: the consistency and lock boundary
An aggregate gathers objects that must stay consistent together into one transactional boundary — and under high traffic it is the most critical design decision, because the aggregate boundary is the lock boundary. A giant Order aggregate (order + all lines + payments + shipments + returns) means a wide lock on every update: two concurrent line updates wait on each other, optimistic-lock conflicts climb, throughput falls. The rule: keep aggregates small — as large as true invariants require, not a millimeter more. 'Order total must be consistent with its lines' is a true invariant; 'order and return must update atomically' usually is not — the return becomes its own aggregate, related by identity (orderId), consistent via events.
Decision table: do these two belong in one aggregate?
Question | If yes | If no |
|---|---|---|
Do they share a same-transaction invariant? | Same-aggregate candidate | Separate aggregates |
Do different users update them concurrently? | Consider splitting (lock contention) | May stay together |
Is one listed/scaled independently of the other? | Separate aggregates | May stay together |
Is delayed consistency acceptable to the business? | Split + domain event | Same aggregate + one transaction |
Domain events: the asynchronous language between boundaries
As aggregates shrink, consistency between them moves to domain events: OrderPlaced triggers stock reservation, email notification, loyalty points — each at its own pace, in its own context. This is the business-layer face of the async architecture from our earlier articles (outbox, idempotent consumers): event schemas are named in the ubiquitous language (not the technical 'OrderTableUpdated' but the business's 'OrderConfirmed'), versioned, and managed as contracts. The high-traffic bonus: the event stream is also the natural feed for read models (CQRS projections).
Pragmatism in the implementation layer
In the Java/Spring world pragmatic DDD looks like this: a module per context (boundary tests with Spring Modulith), a repository per aggregate (no repositories for internal objects), application services drawing transaction boundaries, domain services carrying pure business logic. Two common traps are avoided: the anemic model (all logic in services, entities as data bags — DDD's shape without its substance) and ceremonial DDD (a heap of value objects + factories + specifications on a three-table CRUD module — where the complexity itself becomes the risk). The yardstick never changes: is there a real problem this pattern solves?
Business impact: boundaries put team speed and scale on one map
A correct context map pays off three ways: scale — on campaign day only hot contexts grow, the bill stays focused; team speed — with clear boundaries teams work in parallel without waiting, merge conflicts and 'who broke what' meetings shrink; cost of change — a new business rule touches one context, the regression surface stays small. The aggregate discipline's return reads directly on the performance dashboard: lock wait times and optimistic-lock conflict rates fall.
Frequently asked questions
Does DDD require microservices?
No — a bounded context is a logical boundary; in a modular monolith it lives as a module. Physical separation (a service) happens when scale or team reasons arise; if the boundary is already drawn, that transition can be sliced.
How small should an aggregate be?
As large as true invariants require. In doubt, choose small: a missing invariant can be compensated with events; a giant aggregate's lock cost cannot.
Does ubiquitous language really matter?
At scale, yes: when business and engineering use different words, requirement translation produces defects every sprint. Code, tests and meetings should share one glossary.
How do we start DDD in an existing system?
Not with big-bang modeling: pick the most painful business area, sharpen its language and boundary, isolate it from the legacy model with an anticorruption layer. The strangler fig approach applies here too.
Pragmatic DDD checklist
Context map drawn; relationship types (ACL, customer-supplier) marked
No direct table/internal-model access across contexts
Aggregates sized by true invariants; lock metrics monitored
Cross-boundary consistency via domain events + outbox
Event names in business language; schemas versioned
Module boundaries enforced by tests (Spring Modulith/ArchUnit)
Ceremony audit: every pattern justified by the problem it solves
SSH Yazılım designs domain models and bounded context architecture together with high-traffic goals. Let us draw your boundary map together.