Module Quiz
Complete this quiz after finishing all concept and practice pages.
Current Module Questions
Question 1: The Three-Level Split
In one paragraph, distinguish business domain, subdomain, and bounded context. Give one thing each is NOT.
Answer: A business domain is what the company does as a whole (e.g., "parcel logistics"). A subdomain is a coherent area of that business (e.g., "pricing," "tracking") -- it is an organizational concept, not a software one. A bounded context is a software-design concept: a boundary inside which one consistent model and one ubiquitous language hold. A subdomain can map to one or more bounded contexts; the mapping is not always 1:1. What each is NOT: a domain is not a piece of software; a subdomain is not a service; a bounded context is not merely a namespace or a folder -- it is defined by the boundary of a language and model.
Question 2: Classify the Subdomain and Defend -- Conference Check-In
Conference Ticketing Co. runs the 30-second QR-scan check-in process at the event venue. Classify the check-in subdomain (core / supporting / generic) and defend. Name the buy-vs-build answer, the staffing answer, and one signal that the classification might be wrong.
Answer: Core, conditionally. For a commodity ticketing platform where customers don't care who made check-in, it is supporting -- buy or lightly customize, staff with mid-level engineers, acceptable to have occasional latency. For a platform whose differentiator is "fastest check-in in the industry," it is core -- build in-house, staff senior/principal engineers, every millisecond matters. The signal that your classification is wrong: sales is being won or lost on check-in performance in customer demos -> it is core whether you labeled it that way or not. Classify by what the market pays for, not what feels important internally.
Question 3: Bounded Context Boundary Scenario #1 -- The "Customer" Conflict
At Parcel Shipping Co., four teams use the word customer: Sales (leads and quotes), Shipping (who booked the parcel), Billing (who pays the invoice), and Support (who filed a ticket). Engineers propose a single Customer microservice that all four consume. What is wrong with this proposal and what is the DDD-correct move?
Answer: The four teams use the same word for four different concepts: Sales's Customer has lead status and sales stage; Shipping's has addresses and contracted service classes; Billing's has a billing address, payment methods, and credit terms; Support's has contact preferences and a ticket history. Forcing them into one model either creates a fat Customer owned by no one (write contention, invariants scattered) or a lowest-common-denominator Customer that no one can actually use. DDD move: each context keeps its own Customer model, with a shared CustomerId as the integration key. A thin Customer Directory service can exist for identity (emails, phones, IDs) as a supporting context, but never for "everything about a customer." This is a straight application of the language heuristic: different language -> different context.
Question 4: Bounded Context Boundary Scenario #2 -- Splitting Shipping
A Shipping context has grown large: it owns booking, label generation, driver dispatch for same-day, carrier integrations for ground and air, returns, and exception handling. Engineers propose splitting it along "domain object" lines: ShipmentService, LabelService, CarrierService, etc. Is this the right split? If not, what is?
Answer: Not the right split. Splitting by object ("one service per noun") gives CRUD services that chat constantly and reproduce the old monolith as a distributed monolith. The correct split is along business capability and rate-of-change seams: probably Dispatch (same-day routing, volatile, different language: "driver, leg, route, bag"), Carrier Gateway (integrations with external carriers, volatile per vendor), and Shipping (booking and label generation, stable). Returns may be its own context. Each has its own ubiquitous language, its own team, its own deploy cadence. "One service per noun" is a hint that the team is using OO-modeling intuitions to make strategic decisions; resist.
Question 5: Design an Aggregate -- Event Ticket Hold
Design the TicketHold aggregate for a conference-ticketing checkout. Requirements: a hold reserves up to 8 tickets of one type for 10 minutes while the user pays; if the user commits, it becomes an Order; if the user abandons, the hold releases tickets back to inventory; invariants: at most 8 tickets per hold; cannot extend beyond 10 minutes; once committed, immutable. Produce the aggregate sketch and name its invariants.
Answer:
Aggregate: TicketHold (root entity)
Identity: HoldId (uuid)
VOs: TicketTypeId, Quantity, Money, Instant (created_at, expires_at)
References by ID: EventId, CustomerId, (after commit) OrderId
State: status {ACTIVE | COMMITTED | RELEASED | EXPIRED}
Invariants:
1. quantity in [1, 8]
2. expires_at - created_at == 10 minutes (cannot extend)
3. Cannot add tickets once ACTIVE
4. Can transition to COMMITTED only if status == ACTIVE and not expired
5. Cannot modify after COMMITTED, RELEASED, or EXPIRED (terminal)
6. Releasing emits TicketsReleased event (so Inventory can restock)
Commands: place(ticket_type_id, quantity, customer_id) -> emits HoldPlaced; commit(payment_ref) -> emits HoldCommitted + creates Order; release(reason) -> emits HoldReleased; auto-expire() -> emits HoldExpired + TicketsReleased. Inventory is a separate aggregate -- the reservation side effect crosses aggregates via events, not in the same transaction.
Question 6: Design a Domain Event -- PaymentCaptured
Design the internal domain event and the integration event for "payment captured" in a conference-ticketing payments context. Show fields, versioning, and one field you intentionally keep out of the integration event.
Answer:
Internal domain event (lives in Payments context):
@dataclass(frozen=True)
class PaymentCaptured:
event_id: str
payment_id: str
order_id: str
captured_at: datetime
amount: Money
stripe_charge_id: str # vendor-specific
psp_raw_response: dict # for debugging
risk_score: float # internal fraud model
Integration event payments.payment.captured.v1:
{
"schema": "payments.payment.captured.v1",
"event_id": "uuid",
"payment_id": "pay_...",
"order_id": "ord_...",
"captured_at": "2026-04-10T...",
"amount": { "amount_minor": 19900, "currency": "USD" }
}
Fields kept out of the integration event: stripe_charge_id (PSP lock-in -- if we switch to Adyen, consumers should not break); psp_raw_response (debug noise); risk_score (internal classifier, not a contract). Versioning discipline: additive changes go into v1; if fields must be removed or renamed, we publish v2 and run both for the deprecation runway.
Question 7: EventStorming -- Name the Sticky
For each description below, name the sticky-note color/kind:
- "Carrier Gateway" (a system we do not own).
- "Whenever
ShipmentBookedand service is SAMEDAY, assign a same-day driver." - "CarrierAssigned."
- "AssignCarrier."
- "Carrier Capacity Dashboard -- shown to dispatchers before assignment."
- "Who owns cancellation policy?"
Answer: 1) Pink (external system). 2) Lilac/Purple (policy). 3) Orange rectangle (domain event, past-tense fact). 4) Blue rectangle (command, intention). 5) Green rectangle (read model). 6) Red (hot-spot / open question).
Question 8: Anticorruption Layer vs Open Host Service
A legacy system returns responses like <cOntrAct><cst_num>12345</cst_num>...</cOntrAct>. A new Pricing context consumes it. A different team exposes Pricing's own public API to 7 downstream consumers. Which edge needs an ACL, which needs an OHS, and why?
Answer: The edge from legacy -> Pricing needs an ACL built by the downstream (Pricing). Pricing is a core subdomain and cannot let cst_num, cOntrAct, typ, svc_cd into its core model; the ACL translates the ugly legacy shape into Pricing's domain language once, at the boundary. The edge from Pricing -> 7 consumers needs an OHS + Published Language built by the upstream (Pricing). The OHS decouples Pricing's internal model from what consumers see; the published language is a consumer-friendly contract (e.g., pricing.quote.v1) that Pricing can refactor behind without breaking its downstreams. The distinction: ACL protects a downstream core from a messy upstream; OHS protects many downstreams from an upstream's internal changes.
Question 9: Transactional Boundary Rule
An engineer writes: "In place_order, I load the Order aggregate and the Inventory aggregate, decrement inventory, save both, and commit one transaction." State what is wrong and how to fix it.
Answer: This violates the one-transaction-per-aggregate rule. The Order and Inventory aggregates have separate invariants and separate locking domains; one transaction spanning both couples them at the storage layer and invites lost updates, deadlocks, and broken invariants under concurrency. Fix: the handler calls order.submit() and saves the Order aggregate in one transaction, emitting OrderSubmitted. A handler on the Inventory side consumes OrderSubmitted, loads the affected Inventory aggregate, decrements, and saves in a separate transaction. Consistency is eventual and explicit. If the decrement fails, a compensating event (InventoryInsufficient -> OrderRejected) is emitted and the order is rolled back. This is the saga / event-driven coordination pattern.
Question 10: Repository Anti-Patterns
A ShipmentRepository grows these methods: find_all(filters), bulk_update_status(ids, status), mark_delivered(id), aggregate_by_origin_country(). Identify the anti-patterns and propose fixes.
Answer:
find_all(filters)-- slides into arbitrary-query territory; belongs to a CQRS read model, not the write-side repository.bulk_update_status(ids, status)-- bypasses aggregates entirely; invariants cannot be enforced on a bulk update. If such an operation is genuinely needed (imports, back-office tools), it must be modeled as a series of aggregate commands or as an explicit domain-service operation with an ADR.mark_delivered(id)-- business logic smuggled into the repository; belongs on theShipmentaggregate root asrecord_delivered().aggregate_by_origin_country()-- a reporting query; belongs in a read model / analytics view.
The repository's API should be collection-shaped and identity-based: load(id), save(aggregate), and very few domain-meaningful queries (find_pending_for_customer).
Question 11: Bounded Context Boundary Scenario #3 -- "We Have Microservices"
A team has 14 microservices. Code audit reveals: 3 services write to the same customer table; 2 services share a shared-model library with mutable domain classes; 6 services must be deployed in lockstep during each release. Does this team have bounded contexts? What is the real architecture?
Answer: They have microservices but not bounded contexts. Shared-DB write access across services is a distributed monolith; the shared mutable model library is a distributed monolith; lockstep deploys are a distributed monolith. The real architecture is "monolith with extra network hops." The DDD rescue: pick the contexts (probably 4-6, not 14), merge code where two services are really one context, give each context its own database schema with exclusive write ownership, replace the shared model library with a published language + local translations, and break lockstep deploys by introducing compatible contracts. This is a months-to-a-year project, but the first step is drawing the honest context map and letting the anti-patterns name themselves.
Question 12: CQRS Judgment Call
Three contexts each propose adopting CQRS: (a) Billing where invoices are generated once and read daily by accountants; (b) Tracking where dozens of scan events per parcel feed customer pages and ops dashboards; (c) Pricing where a rate snapshot is published weekly and read on every quote. Which contexts benefit from CQRS and why?
Answer:
- Billing -- marginal. Read and write shapes are similar (invoice rows, accounting views). CQRS here is overkill; a SQL view for the accountants' dashboard is sufficient.
- Tracking -- strongly benefits. The write model (
ShipmentJourneyaggregate, many scan events) and the read models (customer tracking page, ops dashboard) have fundamentally different shapes and scales. Classical CQRS fits. - Pricing -- mild fit. The write side is low-frequency and simple; the read side is high-frequency (every quote reads a rate snapshot) but naturally served by a simple indexed read of the write model. A read cache is sufficient; full CQRS is overkill.
Rule: apply CQRS per context where read and write shapes meaningfully diverge; do not impose it system-wide.
Question 13: Event Sourcing Judgment Call
Two contexts propose event sourcing: (a) Returns where each return goes through 5-15 status events and audits are frequent; (b) Notifications where the system sends emails and SMS and the only state is "has this been sent yet?" Which fits event sourcing, and what warning would you give the team that chooses it?
Answer:
- Returns -- fits. High number of domain-meaningful events per aggregate, strong audit and "what did we know when?" use cases, temporal queries are natural, out-of-order/late events from warehouse scanners.
- Notifications -- does not fit. One or two state transitions per notification (queued -> sent, queued -> failed). Event sourcing adds machinery with no temporal or audit payoff. A classical state table with a
statuscolumn is correct.
Warning to the Returns team: commit to event schema versioning from day one (upcasters), snapshotting strategy, replay tooling, and GDPR strategy (crypto-shredding, not log mutation). Event sourcing is a contract with your future self; be ready to pay it.
Question 14: Architecture-to-Context Alignment
A 25-engineer company has 6 bounded contexts and 22 microservices. Three teams share ownership of two of the services. One database is shared by two contexts. Name the three most urgent alignment fixes in priority order and the reasoning.
Answer:
- Split the shared database so each context has exclusive write ownership of its tables -- the shared DB is the hardest coupling and the one most likely to cause correctness issues (cross-context invariants are impossible to enforce). Until fixed, every other boundary is theater.
- Assign single-team ownership to every service (or merge/split services to match ownership) -- shared ownership across three teams kills autonomy, makes deploy decisions political, and corrupts the context map.
- Reconcile service count to context count -- 22 services for 6 contexts is ~3.7 services per context, which usually means internal CRUD-per-noun splits. Merge services that are internal to one context unless there is a concrete autonomy or scale reason to keep them apart.
Reasoning order reflects blast radius: shared DB > shared ownership > service proliferation. Fixing the first makes the next two tractable; reversing the order is usually wasted effort.
Question 15: Context Evolution
Parcel Shipping has run for 4 years with contexts Pricing, Shipping, Tracking, Billing, Support. Tracking has gradually absorbed ETA prediction, dispatch, driver routing, and proactive customer notifications. The team says they keep stepping on each other. What evolution is required and how do you manage it?
Answer: Tracking has drifted into a super-context. Three signals point to a split: (1) language drift -- "driver," "leg," "bag" are dispatch vocabulary, not tracking vocabulary; (2) rate-of-change drift -- dispatch changes daily, ETA prediction weekly, customer notifications per campaign; (3) team coordination cost indicates fractured ownership. Evolution: extract Dispatch as its own core context with its own team; move Customer Notifications to a supporting context (possibly merged with existing Support/Notifications); keep Tracking focused on journey state, scan dedupe, and status. Manage it: strangler-fig pattern -- introduce a DispatchingPort inside Tracking, build Dispatch as a new service behind the port, dual-run, cut over gradually. Each extraction is its own ADR with consequences and rollback plan.
Interleaved Review Questions
Prior Module Question 1 (S7M1: Architecture styles and trade-offs)
How does the choice between modular monolith and microservices relate to bounded-context alignment?
Answer: Bounded contexts are logical; modular monolith vs microservices is the physical alignment to those contexts. A modular monolith can honor every bounded context with in-process module boundaries -- same deploy unit, separate schemas, no cross-module imports. Microservices impose physical separation, which buys autonomy and isolation at the cost of ops complexity. The architecture choice should follow context design: one service per context is the default for teams ready to run them; a modular monolith is the correct starting point for small teams or early-stage products. The mistake is letting the infra choice dictate the context design ("we have microservices, so we must have N contexts").
Prior Module Question 2 (S7M4: API Design and Contract Evolution)
How does the bounded-context model inform where API boundaries go and how APIs evolve?
Answer: Each bounded context publishes one Open Host Service with a Published Language to the outside world. The API boundary is the context boundary; versioning, compatibility, and deprecation are all properties of the context's contract, not of individual endpoints. Evolution discipline (non-breaking by default, versioned events, deprecation runway) is what makes contexts cheap to refactor internally -- the external contract survives the refactor because the OHS decouples it from the internal model. Without that decoupling, every internal change is a breaking change.
Prior Module Question 3 (S5M3: Transactions and isolation levels)
An engineer argues, "we don't need aggregates because we have SERIALIZABLE transactions." What is wrong with this argument?
Answer: SERIALIZABLE solves database-level serializability, not business invariants. It cannot know that "total weight ≤ carrier limit" is a rule; it only prevents read/write anomalies within one transaction. Furthermore, SERIALIZABLE becomes impossible across services (distributed transactions are an operational nightmare). Aggregates are about consistency boundaries for invariants -- they give you a single place where invariants are enforced in code, independently of the storage engine's isolation level. You still need SERIALIZABLE (or similar) for the underlying row-level guarantees, but that is orthogonal to aggregate design. Aggregates also scope optimistic concurrency to a small unit, which is how DDD systems scale beyond what SERIALIZABLE can deliver.
Prior Module Question 4 (S6M2: Distributed consensus)
Why can't a domain event be "committed" across two bounded contexts atomically?
Answer: Atomic commit across contexts would require a distributed transaction (2PC or equivalent) -- expensive, fragile, and blocked by most modern infrastructure (Kafka does not do 2PC with your DB). The outbox pattern sidesteps the problem: the aggregate change and the outbox row commit in the same local transaction; a separate relay publishes the event at least once after commit. Consumers dedupe by event_id and handle idempotently. You never get "atomic across contexts" -- you get local atomicity plus eventual visibility. That is the honest shape of distributed systems, and DDD's tactical patterns (outbox, idempotent consumer, compensating events) are designed for it.
Prior Module Question 5 (S7M5: Architecture Decision Records)
A team decides to extract Dispatch from Shipping. What should the ADR capture beyond "we are extracting Dispatch"?
Answer: Context: language/rate-of-change/autonomy heuristics that triggered the split; volume numbers; prior evidence of friction (step-on-each-other incidents). Decision: the new context's name, its team, its data store, its deployable boundary. Consequences: new integration edges introduced (what events and which patterns -- partnership? customer-supplier?), new ops surface, expected throughput, latency added to cross-context paths, rollback plan. Alternatives considered: "do nothing and enforce with lint rules," "split differently (by geography, by shipment class)." The ADR is the evidence trail the team can re-read in a year when the decision looks odd in hindsight; a decision without consequences and alternatives is not an ADR, it is a note.
Self-Assessment and Remediation
Mastery Level (90-100% correct): Ready to advance to Module 4 (API Design & Contract Evolution) and Module 5 (Architecture Decision Records) with confidence. Drive the next major bounded-context decision in your org.
Proficient Level (75-89% correct): Re-read the concept pages matching your missed questions. Re-run Kata 2 (draw a context map) or Kata 3 (design an aggregate). Likely weakness: strategic (cluster 1-2) or tactical (cluster 4) -- the quiz is structured so the weakness is easy to locate.
Developing Level (60-74% correct): Rework Clusters 2 and 4 from scratch. Redo Practice 2 (Context Mapping Workshop) and Practice 4 Kata 3 (Checkout Aggregate). Expect the weakness to be: confusing subdomain with context, over-sizing aggregates, or mistaking microservices for bounded contexts.
Insufficient Level (<60% correct): Restart from Cluster 1 Concept 1. The most likely issue is treating bounded contexts as "services" or aggregates as "database entities." Until "what language is spoken here?" and "what invariant lives here?" are reflex questions, the rest of the module does not stick.