Boundaries and contracts between services: bounded contexts, shared kernel and published language
Why 'customer' does not mean the same thing in two services, why the shared types library is a trap, and which Domain-Driven Design patterns healthily organize the relations between services.
The first two chapters described how to organize cross-cutting concepts. This chapter explains why the federated model is the right one, using the conceptual tools of Domain-Driven Design. Because DDD formalized, twenty years ago, exactly the problem we are handling: what happens when the same word designates different things depending on where you stand?
By the end of this chapter, you will be able to read a multi-service system as a map of bounded contexts, situate the six main relationship patterns between contexts, recognize the shared kernel anti-pattern and its symptom (the distributed monolith), and formulate the minimal conventions an ecosystem must publish to stay consistent without coupling itself.
Same word, different meanings: the bounded context
Domain-Driven Design starts from a linguistic observation: in any organization, the meaning of a word depends on context. “Customer” does not designate the same thing for the sales department (a signed prospect), accounting (an account to invoice) and support (a contract holder). Forcing a single definition produces a monstrous model that satisfies nobody.
DDD names bounded context the boundary within which a model and its vocabulary are consistent and unambiguous. The central lesson: do not unify the models, unify the map. You accept several definitions of “customer”, one per context, and you make the relations between contexts explicit.
Verify it yourself on our commerce suite. Browse the three contexts below and observe: no attribute coincides, except the identifier. Your mission: figure out what would happen if these three definitions were merged into a single universal “Customer” entity.
Customer = a visitor with preferences
- customer_id — local identifier
- language, currency
- browsing history
- wish lists
Here, the customer is a personalization profile. No notion of payment or legal obligation: this context does not need them, and its model stays simple precisely because it ignores the rest.
The merge would produce an entity with thirty attributes of which each service would use a fifth, subject to the payment context’s regulatory constraints even to display a wish list, and modified by three teams with incompatible agendas. That is exactly the “monstrous model” the bounded context avoids.
Same name, same intuition, three definitions. They share a link (it is “the same” customer in the user’s eyes), but neither their attributes, nor their lifecycle, nor their invariants coincide. Each service is a bounded context, and that is perfectly fine.
The relationship map: six patterns to know
Once the contexts are laid out, DDD names the possible relations between them: this is context mapping. Six patterns cover most of the territory:
| Pattern | Relation | When to use it |
|---|---|---|
| Partnership | Two teams co-evolve their contexts, linked success | Two services at the heart of the same critical flow |
| Customer-Supplier | Downstream expresses needs, upstream plans for it | The supplier agrees to serve its consumer |
| Conformist | Downstream adopts the upstream model as-is | Upstream does not negotiate (dominant external service) |
| Anticorruption Layer | Downstream translates the upstream model into its own | Protecting your model from an imposed or legacy upstream |
| Open Host Service | Upstream publishes a stable API open to all | A service consumed by many clients |
| Published Language | A common exchange format, published and versioned | Whole ecosystem, broad interoperability |
Two of these patterns deserve a closer look, because they are the two poles of our subject: the shared kernel (which the table deliberately omits, we are coming to it) and the Open Host Service + Published Language pair, which describes exactly the federation contract of chapter 2: each service publishes a stable API (OHS) whose conventions (external references, minimal semantics) constitute a published language (PL).
The anticorruption layer also deserves a word: it is the translation layer a context places at its boundary to convert someone else’s model into its own. When the support service consumes the payment service’s events, it does not store “a transaction”: it translates it into “a possible contact reason”. The foreign model never penetrates; it is translated at customs. That is the concrete mechanism that keeps bounded contexts watertight.
The anti-pattern: the involuntary shared kernel
Facing our three definitions of “customer”, the temptation from chapter 1 comes back in a sneakier form: “since the services are separate, let us at least share the types”. A common library, suite-core, defining the Customer structure once and for all, imported by every service.
DDD calls this pattern the shared kernel: a model fragment jointly owned by several contexts. And it comes with a warning the industry learned the hard way: a shared kernel is only viable between very close teams, continuously coordinated, on a tiny scope. Everywhere else, it produces the worst of both worlds:
- Every evolution of the shared type requires re-versioning and re-deploying all the services importing it: you have recreated the monolith’s evolution coupling.
- But without the monolith’s benefits: no common transactions, no native integration, and the operational complexity of distribution on top.
This outcome has a name: the distributed monolith. Services technically separated, logically welded together. You pay the price of both architectures and collect the dividends of neither.
Note the nuance: publishing a types library from one service, towards its clients (the payment service’s contract, consumed by whoever wants to call it) is healthy: the dependency follows the direction of the call and the provider versions its contract. That is the Open Host Service. What is toxic is the type owned by nobody and imported by everybody.
The healthy pattern: the published language
If you share neither a database (chapter 1) nor types (above), what is left? DDD’s answer is called the published language: a published exchange language, stable, versioned, that each context translates to and from its internal model.
The canonical examples are the great interoperability standards: federated identity protocols, normalized event formats, open API specifications, accounting or banking exchange formats. None shares internal types; all publish a boundary format that everyone implements at home.
For a federated ecosystem, the published language takes a concrete and minimal form, in three conventions:
- A vocabulary and a minimal semantics. What a “shop” is for the ecosystem: a stable key, a display name, membership in an organization. Nothing more: each context enriches locally.
- A normalized identifier scheme. The format of external references (
suite:org/{org}/shop/{key}…): structured for the control plane that emits them, opaque for the services that store them. - The federation contract. Every building block exposes an idempotent upsert by external reference on its topological entities, documented in a versioned API specification.
One documentation page is enough to fix these three conventions. It is the most profitable investment of the whole architecture: it costs one decision, and it saves years of coupling.
The complete map
You now hold the complete reading of the first three chapters:
| Question | Answer | Conceptual tool |
|---|---|---|
| Who owns the topology? | The control plane | Federation (ch. 2) |
| Who owns the business resources? | Each service | Bounded context |
| What does “customer” mean? | One definition per context | Local ubiquitous language |
| What is shared between services? | Identifiers + conventions, never types | Published language |
| How do you protect yourself from a foreign model? | Translation at the boundary | Anticorruption layer |
| How do we converge? | Idempotent reconciliation | Declarative desired state |
| What do we never do? | The types library imported by all | Shared kernel anti-pattern |
This grid applies far beyond commercial software suites: development platforms, industrial systems, internal microservice ecosystems of a large organization, information system mergers after an acquisition. Everywhere, the same tension between integration and autonomy, and everywhere the same way out: sovereign contexts, a published language, declarative convergence.
Check your understanding
1. What does a bounded context designate in DDD?
2. What is the role of an anticorruption layer?
3. Why is the types library shared between all services an anti-pattern?
4. Which pair of DDD patterns describes the federation contract of chapter 2?
Key takeaways
- Each service is a bounded context: “customer” has a legitimate local definition there. You do not unify the models, you unify the map.
- Context mapping names the possible relations: partnership, customer-supplier, conformist, anticorruption layer, open host service, published language.
- The involuntary shared kernel (the types library imported by all) produces the distributed monolith: the monolith’s coupling, the distribution’s complexity, the benefits of neither.
- The published language is the healthy pattern: minimal vocabulary, normalized identifier scheme, federation contract, all versioned and documented.
- Between contexts circulate identifiers and messages, never internal structures; the anticorruption layer translates at the boundary.
- One practical question remains: facing a real system, how do you choose your model, and how do you migrate from one to another? That is the subject of the last chapter.