System Design · Modeling Boundaries
Domain-Driven Design Concepts
System design interviews fail when every box is named “User Service” and every noun means three things. Domain-Driven Design gives you vocabulary for , , and consistency units ()—so your architecture matches how the business actually talks.
This is not a book report on repositories and factories. It is DDD applied to service borders, events, , and you can draw on a whiteboard in 10 minutes.
Modeling journey
Same words in talk, code, and APIs.
One model per meaning of “Product.”
Consistency boundaries, not god objects.
ACL, events, conformist—on purpose.
Why interviewers care about DDD ideas
When “Product” means sellable SKU in marketing, shelvable unit in warehouse, and invoice line in finance, a single products table shared by three services will fight you forever. Separate with explicit integration beats one fuzzy model.
Focus modeling energy on the —the differentiator. Use simpler patterns or vendors for generic email delivery. Saying that out loud in an interview shows judgment, not laziness.
Ubiquitous language
is a discipline: rename code when product renames concepts. If support says “ticket,” do not keep IssueEntity and CaseDTO for the same thing unless they truly live in different contexts.
Practical technique: write acceptance scenarios with domain words, then make type and API names match. Ambiguous terms become discovery sessions: “Account” might split into BillingAccount and LoginIdentity.
Smell → Fix
"processOrder()" does inventory + charge + email
→ PlaceOrder (Orders), ReserveStock (Inventory), CapturePayment (Billing)
Shared enum ProductStatus used by Catalog and Warehouse
→ Catalog: Draft|Published
Inventory: InStock|Quarantine|Damaged
Integrate via events, not one enumBounded contexts as system borders
Read the diagram definition
flowchart LR
subgraph Catalog["Catalog BC"]
P1["Product (sellable)"]
end
subgraph Inventory["Inventory BC"]
P2["SKU stock"]
end
subgraph Fulfill["Fulfillment BC"]
P3["Shipable item"]
end
subgraph Billing["Billing BC"]
Inv["Invoice line"]
end
Catalog -->|"Published language: ProductId, SKU"| Inventory
Catalog -->|"events ProductPublished"| Fulfill
Inventory -->|"ACL from WMS legacy"| WMS[Legacy WMS]
Fulfill -->|"OrderReadyToShip events"| Billing
Billing -->|"Conformist to tax vendor API"| Tax[Tax SaaS]Drawing contexts before drawing Kubernetes services prevents accidental distributed monoliths. Two contexts can live in one deployable (modular monolith) if teams and scale allow; one “service” that mixes three languages is still three contexts fighting.
Integration styles between contexts belong on a : upstream Catalog publishes ProductId; Inventory may be a to Catalog identifiers; Fulfillment uses an toward a legacy WMS; many consumers share a for events.
Aggregates: where transactions stop
Read the diagram definition
flowchart TD
Cmd["Command: AddItemToOrder"] --> Root["Order aggregate root"]
Root --> Inv["Check invariants: currency, max lines, state=OPEN"]
Inv --> Lines["OrderLine entities / VOs"]
Lines --> Commit["Single TX commit"]
Commit --> Evt["Domain event: OrderItemAdded"]
Evt --> Out["Outbox → other contexts"]
Bad["Anti-pattern: TX across Order + Inventory + Customer"] --> Split["Separate aggregates + saga/events"]The aggregate root (e.g. Order) is the only entry point for changes. External objects hold orderId, not pointers into line-item rows. Invariants—“all lines same currency,” “cannot add items when Cancelled”—are enforced in one place.
Size rule of thumb: if two pieces must be strongly consistent for a user-visible rule, they may share an aggregate. If they only need eventual agreement (stock counts vs marketing copy), separate them. Huge aggregates serialize all writes and become hotspots.
have identity; like Money do not. Prefer value objects for money and quantities so rules travel with the data. A holds logic that does not fit a single root without turning the root into a god object.
Worked invariant — seat hold aggregate
Hold aggregate root: HoldId
Contains: SeatRefs (VOs), ExpiresAt, Status
Invariant: seats unique; expire → Released in same TX as status flip
Not in aggregate: Payment (separate); User profile (separate)
Across boundary: HoldConfirmed event → Orders creates Order aggregate
Why not one TX with Payment?
Payment provider latency + PCI boundary
Saga: confirm hold → capture payment → compensate hold on failureDomain events as integration
A is something that already happened in ubiquitous language: OrderPlaced, SeatHoldExpired. Other contexts subscribe and update their own models. That is how you avoid shared databases while keeping the system coherent.
Implementation companions for system design: transactional outbox (event commit with aggregate), idempotent consumers, versioned schemas, and clear causality (correlation / causation ids). Event-driven architecture notes go deeper on brokers; here the point is modeling—events are facts from a context, not a dump of row changes.
Prefer business events over raw CDC for inter-context contracts. CDC is a great feed into an ACL or analytics; it is a weak published language for product workflows.
Anti-corruption layers
Read the diagram definition
sequenceDiagram
autonumber
participant Core as Your Fulfillment
participant ACL as Anti-corruption layer
participant Leg as Legacy WMS
Core->>ACL: ReserveStock(sku, qty) in domain language
ACL->>ACL: map SKU → LEGACY_ITEM_CODE
ACL->>Leg: XML allocateItem
Leg-->>ACL: opaque status codes
ACL->>ACL: map to StockReserved / StockDenied
ACL-->>Core: domain result
Note over Core: Core never imports WMS XML typesThe owns mapping tables, protocol quirks, and weird status codes. Your core domain imports only your types. Cost: another component to test. Benefit: you can replace the legacy system without rewriting the core.
Choose ACL over when upstream is unstable, poorly named, or spans concepts you must split. Choose conformist when upstream is a clean published language you are happy to adopt (e.g. a central Identity context’s user id).
Context maps on the whiteboard
A is the DDD artifact that maps cleanly to system design: boxes are contexts; arrows are relationship types, not just “HTTP.” Label upstream/downstream so everyone knows who adapts to whom.
| Relationship | Meaning | SD implication |
|---|---|---|
| Published language | Shared integration dialect | Versioned events / API schemas |
| ACL | Translate foreign model | Adapter service / module |
| Conformist | Adopt upstream model | Fewer translations; coupled shape |
| Customer–Supplier | Downstream needs negotiated | Contract tests, SLAs |
| Shared kernel | Small shared model both edit | High coordination cost—use sparingly |
| Separate ways | No integration | Honest: duplicate rather than couple |
Interview pattern: from story to boundaries
Prompt: Design a ticket marketplace
1. Language pass
Hold, Listing, Order, Payout, Transfer — define each
2. Context candidates
Listings (sellers), Checkout (buyers), Payments,
Delivery/Transfer of tickets, Risk/Fraud, Search
3. Aggregates
Listing: tickets available count + price rules
Hold: seats + expiry (short TTL consistency)
Order: payment intent reference + line items
Do NOT one-TX Listing+Payment+Email
4. Context map
Listings upstream of Search (events)
Checkout customer of Payments (ACL if using Stripe shapes)
Risk conformist to device fingerprint vendor? or ACL?
5. Say out loud
"Search can lag; Holds cannot."
That single sentence is DDD applied to SLOs.What to skip in an SD interview
You rarely need to lecture on factories, repositories, or layered architecture diagrams from the blue book. Interviewers want: clear contexts, sensible aggregates, event vs sync edges, and how you protect the core from legacy. Mention repositories only if asked about persistence boundaries.
Likewise, “we use DDD” is empty. Show a context map and one invariant. That is the signal.
Trade-offs to weigh
Small aggregates vs fewer distributed workflows
ACL purity vs shipping speed
Shared kernel vs duplication
One enterprise model vs many contexts
Domain events vs sync APIs
Choosing and explaining domain-driven design concepts
A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.
Explain the core idea
Use ubiquitous language and bounded contexts to place system borders where meanings and invariants naturally split.
Aggregates define consistency units; cross-aggregate work uses domain events and explicit workflows.
Context maps name integration styles—ACL, conformist, published language—so legacy and vendors do not corrupt the core model.
When it belongs
Designing multi-service systems where the same word means different things across teams.
Explaining why two domains cannot share one writable schema.
Integrating with legacy ERPs or third-party APIs without leaking their models inward.
How it fails
God aggregates that serialize all writes and hide unrelated invariants.
Shared “enterprise” tables that merge Catalog, Inventory, and Billing meanings.
Event storms of CDC row dumps mistaken for a published language.
Trade-offs to weigh
Aggregate size versus saga complexity.
ACL isolation versus integration speed.
Model clarity in many contexts versus translation overhead.
Common misunderstandings
Reciting repository/factory patterns instead of drawing boundaries.
Assuming one microservice equals one bounded context automatically (or vice versa).
Using DDD jargon without stating a single invariant or context relationship.
Wrap-up
Exercises
- Split “Product.” List attributes for Catalog vs Inventory vs Billing. Which become separate models, and what ids cross the boundaries?
- Size an aggregate. Shopping cart with coupons and inventory checks: what is inside the Cart aggregate, and what becomes events/sagas?
- Draw a context map. Your system plus Stripe plus a legacy warehouse—label ACL, conformist, and published language edges.
Topic-specific references
- Evans: Domain-Driven Design Reference — concise definitions of the patterns used here.
- Evans: Domain-Driven Design (book) — deeper treatment when you need more than interview framing.
- Fowler: Bounded Context — short bliki primer aligned with system boundaries.
Key takeaway
DDD for system design is language, boundaries, consistency units, and deliberate translation. Draw the context map, name the invariants, and keep legacy models behind an ACL—leave the blue-book taxonomy for when someone asks.