Domain Message Flow and Heuristics for Finding Boundaries
What This Concept Is
Once EventStorming has produced a timeline, the next question is where to cut it into bounded contexts. A domain message flow is a sequence diagram-style view showing which bounded context emits which command, event, or query, and which receives it. It is the zoomed-in complement to the context map: the map shows the topology; the flow shows the traffic.
Heuristics for finding boundaries are the "where should the lines go?" rules you apply on top of a message flow. The DDD community has codified a library of these (Nick Tune, dddheuristics.com). The highest-value ones:
- Language heuristic. Where the vocabulary changes, there is probably a boundary. A "shipment" to Pricing is a financial item; to Tracking it is a physical parcel. Different ubiquitous language -> different context.
- Actor heuristic. Where the user persona changes, there is often a boundary. If dispatchers and support agents work with different screens using different terms, those are candidate contexts.
- Rate-of-change heuristic. Concepts that change together belong together; concepts that change at different cadences belong apart. If pricing rules change weekly and carrier integrations change quarterly, don't merge them.
- Subdomain heuristic. Core, supporting, generic subdomains suggest different contexts. You rarely want a core subdomain fused with a generic one.
- Autonomy heuristic. Contexts should be independently deployable; if two chunks of code must deploy together every time, the boundary between them is fiction.
- Business-capability heuristic. Each context provides one coherent business capability ("price a shipment", "track a parcel"). If you cannot state the capability in 8 words, the context is too broad or too thin.
- Data-ownership heuristic. Two contexts should not co-own the same piece of data. Whoever enforces invariants on a piece of state owns it.
- Frequency-of-interaction heuristic. High-frequency chatty interactions between candidate contexts signal they should probably merge. Cross-context traffic should be events, not chatter.
- Volatility heuristic. Isolate the most volatile areas so change does not ripple.
A typical boundary decision cites 3-4 of these heuristics explicitly.
Why It Matters Here
Boundaries are the most expensive architectural decision you will make because they are the hardest to undo. A single cross-boundary aggregate or a backward data dependency can lock in coordination costs for years. Heuristics are how you avoid accidental boundaries.
A domain message flow keeps the conversation honest: it replaces "I feel like Shipping and Tracking should be one service" with "here are the 4 messages they exchange per parcel and here is the volatility profile."
Concrete Example
Case: Parcel Shipping Co. -- "Book and deliver" domain message flow
After EventStorming produced candidate contexts (Pricing, Shipping, Tracking, Billing, Carrier Gateway), we draw the message flow for a single happy-path parcel:
Reading the flow with the heuristics:
- Language. Pricing emits
QuoteRequested/RateSnapshot-- purely financial. Shipping emitsShipmentBooked-- operational. Tracking consumes carrier scans and publishesDeliveryConfirmed. Different vocabularies -> likely distinct contexts. ✅ boundary supported. - Actor. Customer interacts with Pricing and Shipping; dispatchers interact with Shipping; support agents interact with Tracking and Billing. ✅ boundary supported.
- Rate-of-change. Shipping booking logic (SLAs, commitments) is stable; Pricing rules change weekly; Carrier Gateway changes when a carrier does. ✅ strong argument to keep them apart.
- Autonomy. Shipping can deploy independently of Pricing if Pricing's contract is stable, because the only sync call is a rate lookup and Shipping caches a rate snapshot. ✅
- Data ownership. Shipment data owned by Shipping; Rate history owned by Pricing; Journey / Scan data owned by Tracking; Invoices owned by Billing. No two contexts write the same row. ✅
- Frequency. Shipping ↔ Pricing is 1-2 sync calls per shipment creation, then never again. Shipping ↔ Tracking is one event per shipment. Low chatter. ✅
Where the heuristics suggest a boundary shift:
- If Shipping called Carrier Gateway per event to recompute ETA, that would be chatty -- merge or move the ETA calc to the Gateway.
- If Billing had to ask Tracking "is this parcel delivered?" synchronously for every invoice, that would be chatty -> move delivery confirmation from "query" to "pushed event" (it already is in the flow above -- good).
A bad message flow for contrast
Before this design, Parcel Shipping had a single ShipmentService that called pricing.db directly and owned carrier adapters internally. That message flow looked like:
Customer --> ShipmentService --> (reads) pricing.db
Customer --> ShipmentService --> (calls) fedex.com / ups.com / dhl.com directly
ShipmentService --> ShipmentService.invoices table
ShipmentService --> ShipmentService.journey table
Heuristic violations:
- data ownership: invoices in the shipment table
- autonomy: pricing and shipping must deploy together
- language: "shipment" means three different things in the same code
- actor: same screens serve dispatchers, support, accounting, and customers
- rate-of-change: every pricing change required a shipment deploy
When a single service triggers five heuristic violations, boundaries are overdue.
Common Confusion / Misconception
"Heuristics are rules -- follow them strictly." They are tie-breakers. In each real decision, two heuristics usually point different directions. The discipline is naming which heuristics you weighted and why.
"Low chatter always means correct boundary." Low chatter means the boundary costs little today. Language and actor heuristics still matter because they predict future coupling costs.
"We did EventStorming, so the boundaries are set." EventStorming gives candidates. The message flow + heuristic pass converts candidates into decisions.
"A message flow is a UML sequence diagram." It is sequence-diagram-shaped but participants are bounded contexts, not classes or services. The granularity is deliberately strategic.
"If two contexts exchange many events, they should merge." If events are one-way and asynchronous, this is not a signal to merge. Events decouple; sync calls couple. Count sync calls, not events.
"There is always a correct boundary." There is a boundary that matches the forces you can see today. A year from now you may re-cut some. Concept 9 is about that.
How To Use It
- Start with a candidate list of 4-8 contexts from EventStorming.
- Draw a message flow for 1-2 central scenarios (happy path + one hard case).
- Walk each edge of the flow and cite heuristics:
- "This edge respects the language heuristic because…"
- "This edge violates the autonomy heuristic because…"
- For each violation, decide: redraw boundary, or redesign the edge (async, ACL, events).
- Produce a decision memo with:
- final boundaries
- named heuristics supporting each
- explicit heuristic trade-offs you accepted
- Feed the resulting boundaries back into the context map (concept 6).
Check Yourself
- You have two candidate contexts that exchange 60 synchronous calls per workflow. Which heuristic is the strongest argument to merge them?
- Pricing rules change weekly; Tracking logic changes twice a year. Which heuristic is most relevant to keeping them apart?
- Give an example of two heuristics that disagree on a boundary decision.
Mini Drill or Application
For either Parcel Shipping or Conference Ticketing:
- Draw a domain message flow for one non-happy-path scenario (e.g., delivery exception, payment failure, event cancellation).
- Walk the flow and cite at least 4 heuristics per boundary.
- Identify at least one boundary where two heuristics disagree, and document how you chose.
- Write 1-paragraph "risk note" on the most fragile edge.