Phase F gave you the machinery of event-driven systems. Phase G asks the harder question: how do you decide what should even be a service? The design vocabulary from Eric Evans's 2003 blue book — bounded contexts, ubiquitous language, aggregates, context maps. The layer that separates thoughtful microservices from accidental distributed monoliths.
An honest observation about the last decade of "we moved to microservices": a majority of the resulting systems are worse than the monoliths they replaced. Not because microservices are wrong — they solve real problems at scale — but because most teams got the boundaries wrong. They took a monolith, drew arbitrary lines through it (usually one service per team, or one service per database table), added network calls between the pieces, and shipped what they called "a microservices architecture." What they actually shipped was a distributed monolith — all the coupling of a monolith, none of the local-call performance, plus the operational overhead of ten services.
The symptom pattern is remarkably consistent. If you've been through it, you'll recognize every line below. If you haven't, this is the shape of the trap.
orders-service requires coordinated deploys of payments-service, shipping-service, and inventory-service — otherwise integration tests fail." Nobody deploys anything alone.// Root cause: services share too much — either code, contracts, or database schema. Not really independent.customers table. When we tried to change the schema, we broke three of them." Half your "microservices" share one database.// Root cause: data ownership wasn't defined. Nobody actually owns the model — everyone reads and writes it.customer mean here?' — Sales says account manager assigned, Support says open tickets, Shipping says delivery address. The class has 47 fields." Three domains share one model that fits none of them.// Root cause: one model tries to serve multiple bounded contexts. The result: nobody's happy.notifications-service is down, checkout fails. When reviews-service is slow, the homepage times out." Independent services aren't independent in practice.// Root cause: synchronous coupling across contexts that should be async or decoupled entirely.Every one of those symptoms has the same root cause underneath: the boundaries were drawn based on something other than the actual design of the domain. Sometimes it's the org chart (Conway's law, applied without thought) — one team, one service. Sometimes it's the database schema — one table, one service. Sometimes it's the tech stack — one Rails app, split into "microservices" that share the same models and migrations. None of these give you actual boundaries. They give you deployment units that happen to be separated by network calls — which is the worst of all possible worlds. You get all the complexity of distributed systems with none of the decoupling benefits that were the whole point of doing it.
The alternative isn't "don't do microservices." It's design the boundaries before you draw them. Understand the domain first. Find the language seams — where the same word means different things to different people. Find the transaction seams — what has to be atomic together, and what merely has to be eventually consistent. Find the rate-of-change seams — what pieces evolve together, and what pieces evolve independently. The service boundaries follow. They're not arbitrary; they're read out of the domain by someone who took the time to look. That's what Phase G is about. This module covers the vocabulary; the rest of the phase covers everything you have to build to make it work.
The concept that starts everything in Domain-Driven Design is the bounded context — a boundary within which a single, consistent model applies. The same real-world term can (and usually does) mean genuinely different things to different parts of an organization, and the classic mistake is trying to build one universal model that captures all of them. That's what produces the 47-field Customer class that nobody's happy with. The DDD move: let each context have its own model, its own vocabulary, its own database. If the same person is a "Customer" in three contexts, that's three different Customer types, one per context. They may share an identifier (customer_id) but nothing else about the model needs to match.
A person the sales team has a relationship with. Cares about: account manager, contract terms, lifetime spend, upsell opportunities, contract renewal date.
A destination the logistics system needs to route packages to. Cares about: current shipping address, delivery instructions, signature required, timezone.
A person the support team has case history with. Cares about: open tickets, escalation status, satisfaction score, previous resolutions.
Look carefully at those three cards. They have almost nothing in common except the customer_id. Sales doesn't care about delivery instructions. Shipping doesn't care about lifetime value. Support doesn't care about contract renewal dates. If you built one Customer class that combined all three, it would have 20+ fields, 60% of which are irrelevant to any given caller. Every change to the model would require reviewing whether Sales, Shipping, or Support cares. Every migration would touch three teams. The class would be the shared coupling point that prevents anyone from moving fast. The bounded contexts pattern says: don't do that. Let each context have its own model. The customer_id is the shared identifier; everything else is contextual.
Once you accept that each context owns its own model, several other DDD concepts fall out naturally. Ubiquitous language is the rule that within a bounded context, everyone — code, docs, PMs, on-call runbooks — uses the same terms with the same meanings. If Sales calls it a "lead" and the code calls it a "prospect," you have a translation cost every time anyone talks to anyone. If they match, they don't. Aggregates — the concept we touched briefly in event sourcing (M.28) — are the transactional consistency boundaries inside a bounded context: what has to be atomic together. Both concepts serve the same underlying idea: make the boundaries between contexts explicit, and let each context be internally consistent on its own terms.
A boundary within which a single model applies consistently. Outside the boundary, the same terms may mean different things. Usually maps directly to a service (or a small group of services owned by one team). Contains its own database, its own ubiquitous language, its own aggregates. Communicates with other contexts through explicit, well-defined interfaces — never by directly reading another context's tables.
The set of terms used consistently across code, docs, tests, PMs, and support runbooks within a bounded context. If the business people say "order fulfillment," the code should not say order_processing. If the concept is "checkout session," the database column should not be cart_state. The rule Eric Evans emphasizes most: language mismatches between business and code are the primary source of accidental complexity. Boundaries between languages ARE boundaries between contexts.
The unit of transactional consistency inside a bounded context. An Order aggregate might include Order, LineItems, and OrderStatus — all changed together, all loaded together, all persisted together. The aggregate root is the one entity that outside code can reference by ID; everything else is internal. In event sourcing (M.28), aggregates are what emit events. In service design, aggregates define the natural unit of "what a command changes." Cross-aggregate consistency is eventual (via sagas from M.30) — not transactional.
The single biggest practical takeaway: service boundaries should align with bounded context boundaries. A service that spans two bounded contexts inherits the coupling of both — you'll end up with the 47-field Customer class, or the shared database, or the ambiguous ubiquitous language. A bounded context that spans two services inherits the fragility of cross-service coordination — every internal operation becomes a distributed transaction. The ideal, and the pattern most large-scale event-driven architectures converge toward: one bounded context per service; one team per bounded context; explicit contracts at every context boundary. The org chart, the code, and the domain are three views of the same shape.
Bounded contexts don't exist in isolation — the Sales context needs to know about products from the Inventory context; Shipping needs to know about orders from Sales; Support needs to know about everything. How contexts relate to each other has as much impact on system design as the contexts themselves. Eric Evans named these relationship patterns context maps, and understanding them is what separates "we drew boundaries" from "we understand our system's shape." Every cross-context relationship is one of a handful of well-known patterns. Naming them correctly gives you a shared vocabulary for talking about system evolution.
Salesforce-adapter, legacy-erp-gateway, third-party-shipping-facade are all ACLs.MoneyValue type across Sales and Payments). Legitimate only for genuinely universal concepts; frequently abused as "we share the whole domain model" — which reintroduces the coupling bounded contexts are supposed to eliminate. If your "shared kernel" has more than 3-5 types, it's actually a hidden shared database.The Anti-Corruption Layer deserves special mention because it's the pattern most teams underuse — and the one that pays back its investment fastest. Whenever your context needs to integrate with something you don't control — a legacy system, an external vendor, a badly-designed upstream service that your team doesn't own — build a translation layer at the boundary. The ACL is a small adapter (often a separate module, sometimes a separate service) whose only job is to convert the upstream's model into your context's model. Your internal code talks only to the clean, well-designed adapter interface. When upstream changes their model, only the ACL changes; the rest of your context keeps working. The pattern is defensive engineering: it treats other people's models as untrusted input, and gives your context a controlled surface area to defend.
One last observation about context maps: drawing the actual map for your system is one of the highest-leverage exercises in engineering. Half a day with your team, sticky notes on a wall, listing every context in your system and every relationship type between them — you learn more about your architecture than a month of reading code. The messy parts of the map (the conformist relationships to a legacy system nobody wants to own, the shared-kernel abuse across four contexts, the missing ACL between your service and a flaky vendor) — those are exactly the parts that cause your on-call incidents. Making the map visible is the first step to fixing them.
Below: an e-commerce domain with fifteen concepts — Customer, Cart, Order, LineItem, Product, StockLevel, Warehouse, Reservation, Payment, Invoice, Refund, Shipment, Address, Carrier, Review. Pick one of four ways to draw service boundaries around them. Watch the concept tiles regroup by context, the metrics update, and the letter grade change. Three of the four splits are common patterns from real production systems; only one is the boundaries Eric Evans would draw.
Bounded contexts sound elegant in the abstract; finding them in a real domain is where the actual work is. Five heuristics repeatedly surface the right boundaries — no single one is sufficient, but taken together they converge on the answer. If four out of five point to the same seam, that's almost certainly where the context boundary belongs. The teams that get this right typically use all five explicitly, in workshops or design docs; the teams that don't usually rely on the org chart and Conway's law by default — which produces the distributed monolith described in §01.
The primary DDD heuristic. Wherever the same term takes on different meanings — "Customer" in Sales vs Shipping, "Product" in Marketing vs Inventory, "Order" in Sales vs Fulfillment — you've found a context boundary. The language is the fingerprint of the underlying domain concepts. The exercise: ask three teams to define the same word; count how many different answers you get.
The aggregate-boundary heuristic (from M.28 and this module). Whatever must change together atomically belongs in the same aggregate; whatever can be eventually consistent belongs in a different one — often in a different context entirely. Placing an order requires atomic updates to Order + LineItems; it does NOT require atomic updates to Inventory reservation (that's a saga). Wherever you notice yourself wanting a two-phase commit across services, you've probably drawn the boundary in the wrong place.
Things that always change together belong in one context; things that change independently belong in different contexts. If every deploy of one service requires coordinated deploys of another, they're the same context — they just have a network call between them for no benefit. Conversely, if two "services" have never had a coordinated deploy in six months, they're genuinely independent — which is exactly what bounded context boundaries should give you. Deploy independence is the concrete evidence that boundaries are real.
Conway's Law is famous: "systems tend to reflect the communication structure of the organizations that build them." This isn't optional — it will happen. The DDD move is to use it deliberately: align bounded contexts with team boundaries, so each team owns its context end-to-end (code, database, deploys, on-call). Cross-team ownership of one context is the primary source of "who owns this?" incident-response confusion. If two teams both edit one codebase, you have a single bounded context spread across two teams — which is worse than either the merged team or the split context.
Event storming is a lightweight modeling technique: put the team in a room with sticky notes, write every business event that happens in the domain ("OrderPlaced", "PaymentCaptured", "ShipmentScheduled") on orange notes, group them by who reacts to each. The clusters that emerge are almost always the bounded contexts. Rate-of-events is a strong secondary signal: contexts with 100 events/day are natural neighbors; a context with 100 events/day next to one with 1 event/hour usually shouldn't be one service (very different scale needs).
The pattern that reliably works: use language seams as the primary heuristic, verify with transaction and rate-of-change seams, align with team seams, refine with event storming. When multiple heuristics agree, the boundary is real. When they disagree, dig in — the disagreement usually reveals an interesting seam that isn't obvious from any single view. The disagreements are the discoveries. A concept that lives in one team's language but changes at the rate of another team's release cadence is a concept that's about to become a coordination bottleneck, and finding it before it does is exactly what these heuristics are for.
One meta-observation about the whole DDD toolkit: it's easy to over-invest in it and easy to under-invest in it, and both failure modes are common. Over-investment looks like "we spent six months on domain modeling before writing any code and now we have a Beautiful Model™ that hasn't survived contact with reality." Under-investment looks like "we called every noun a service and shipped a distributed monolith." The healthy middle: use DDD as a design language for reviewing boundaries at key inflection points — when adding a new capability, when refactoring an existing one, when splitting or merging services. Not as a religion; not as an omission. A vocabulary you reach for when you need to make a decision, and put down otherwise.
The terms every "we're adopting DDD" doc, every architecture review, every Eric Evans blue-book citation assumes you already know.
Test the DDD intuition. Click an answer; explanation drops in instantly.
Perfect. Bounded contexts, ubiquitous language, aggregates, context maps — all yours. Now: once you have proper service boundaries, how do clients even talk to them without knowing about all N of them?
The vocabulary that separates thoughtful microservices from accidental distributed monoliths.
Same word, different meanings across teams = different bounded contexts. Modeling three "Customer" concepts as one class produces the 47-field class nobody's happy with.
No shared databases across contexts. Communication happens through explicit contracts. Anti-corruption layers defend your model from things you don't control.
Language seams, transaction seams, rate-of-change seams, team seams, event storming. When multiple heuristics agree, the boundary is real. Disagreements are the discoveries.