Inventory exposes one endpoint to Storefront, Fulfilment, and Replenishment:
GET /availability/{sku}?location={location}The route, authentication, media type, and JSON shape remain unchanged. One
release quietly redefines available_quantity. Yesterday it meant physical
stock at the location. Today it means stock which may still be promised after
reservations and holds.
Every request still returns 200. Schema validation still sees an integer.
Storefront, Fulfilment, and Replenishment now act on a different fact.
The service contract survived. The language contract did not.
An Open Host Service and a Published Language solve different problems. The service defines how supported consumers obtain a capability. The language defines what the information exchanged through that capability means. They often appear together, but one API version cannot describe both honestly.
The service makes access reusable
Eric Evans’s DDD Reference describes an Open Host Service as a protocol exposing a coherent subsystem of services for consumers which need integration. Context Mapper places the OHS role on the upstream side of an upstream/downstream relationship.
For the synthetic Inventory service, the protocol includes more than a route:
route GET /availability/{sku}?location={location}
authorization inventory.read
media type application/vnd.inventory-availability+json
unknown SKU 404
invalid location 422
rate limited 429
request limit 600 per minute
timeout 800 ms
change notifications Inventory Developer Relations
support Inventory OperationsThose details decide whether three consumers can use the service without negotiating a private integration each. An endpoint alone is not an Open Host Service. The provider needs a coherent supported-consumer policy, an owned change path, and operational behavior consumers can rely on.
The word “open” does not mean public or unauthenticated. It means the protocol is deliberately available as the common integration path rather than designed around one downstream’s private call sequence.
This contract can break while the payload meaning stays intact. Changing the
required authorization scope, cursor lifetime, error behavior, or request
limits may prevent an old consumer from obtaining the same availability fact.
That is a service migration even when inventory-availability-v1 remains
unchanged.
The language makes meaning publishable
The Published Language answers a different question. Evans describes it as a documented language used to translate between bounded contexts. Context Mapper likewise places the PL role on the upstream and shows it alongside OHS in the same relationship.
The synthetic v1 message is small:
{
"language": "inventory-availability",
"version": 1,
"sku": "sku-1001",
"location": "hel-01",
"available_quantity": 7,
"unit": "item",
"status": "available",
"observed_at": "2026-07-22T10:15:00Z"
}Its JSON Schema can say that available_quantity is an integer. The Published
Language must say that it is the number of whole items Inventory currently
permits new sales to promise after reservations and holds. It must also define
the unit, the meaning of observed_at, the allowed status values, optionality,
and what consumers do with unknown fields, enums, and versions.
Without those definitions, the schema preserves shape while meaning drifts. Generated clients then distribute the same semantic error efficiently.
The language can also outlive HTTP. The same documented availability fact
could cross a message broker or a batch export. Delivery mechanics would add
their own contracts, but sellable_quantity should not acquire a new meaning
because its encoding moved.
One version number hides two change axes
Suppose Inventory needs to expose physical stock separately from sellable stock. Silently changing v1 would break consumer decisions, so the synthetic fixture introduces language v2:
{
"language": "inventory-availability",
"version": 2,
"physical_quantity": 11,
"sellable_quantity": 7,
"unit": "item",
"status": "available"
}The endpoint can remain service v1 while both language versions are supported. Conversely, Inventory can introduce service v2 for a breaking authorization change while continuing to carry language v1.
The compatibility matrix has two axes:
| Change | Service version | Language version | Consumer work |
|---|---|---|---|
| Split physical from sellable stock | 1 | 2 | Translate the new meaning |
| Change authorization scope | 2 | 1 | Migrate access configuration |
| Add a proven optional field | 1 | 1 | Old consumers demonstrate tolerance |
| Remove a decision-bearing status | 1 | 2 | Migrate decisions before removal |
Calling all four changes “API v2” loses the reason for migration. It also makes compatibility claims vague. Does v2 mean the old consumer can still connect, still parse, or still make the same decision? Those are different claims.
Compatibility belongs to deployed consumers
Provider schema checks are useful, but they are not the final witness. The fixture keeps canonical examples and runs them through three consumer-owned adapters:
- Storefront decides whether to offer an item for sale.
- Fulfilment decides whether an accepted order may allocate stock.
- Replenishment decides whether sellable stock has crossed its restock threshold.
The first two still consume language v1. Replenishment has deployed v2. All three use service v1.
That distinction matters because “backward compatible” depends on a direction and a format. Confluent’s schema-evolution documentation separates backward, forward, full, and transitive compatibility, explains how they constrain producer and consumer upgrade order, and notes that the exact rules vary across Avro, Protobuf, and JSON Schema.
A registry can reject a structurally incompatible schema. It cannot prove that Fulfilment still allocates correctly after the provider changes the meaning of an integer. The consumer adapter and its decision test supply that evidence.
Unknown input needs an explicit policy too. The synthetic consumers ignore
unknown fields so compatible additions can pass. They reject unknown language
versions and decision-bearing status values. Defaulting an unfamiliar status
to unavailable might look conservative, but it invents a business decision
and hides the need to migrate.
A retirement date is not a migration plan
Inventory wants to remove language v1 no earlier than 22 October. The date does not make removal safe. Storefront and Fulfilment still record v1 as their deployed language, so retirement remains blocked.
The migration registry retains, per supported consumer:
current service version
current language version
target language version
migration owner
deployment status
executed contract evidence“Storefront is ready” is weaker than a v2 canonical example passing through the deployed Storefront adapter. A pull request, generated client, or successful schema check is progress, not deployment evidence.
Dual publication can provide the transition window when its operational cost is acceptable. Inventory may serve v1 and v2 representations while consumers move independently. Version telemetry shows which contracts are still used; the consumer registry says whether those users remain supported. Neither is enough alone. Traffic can be absent during a quiet period, and a registry can be stale.
Removal becomes credible when every supported consumer records v2 with owned, executed evidence and the earliest removal date has passed. Until then, the old language is production behavior, not historical clutter.
OHS and PL do not settle the downstream design
A Published Language does not require every consumer to adopt the provider’s model internally. Storefront and Fulfilment can translate the same published fact into different decisions. When the upstream language is adequate, a consumer may choose Conformist. When it would distort the downstream model, an anti-corruption layer can own the translation.
Nor does every integration need an Open Host Service. A capability used by one downstream with genuinely specific requirements may deserve an explicit consumer contract instead of a prematurely general host protocol. A Published Language can still be useful between those two contexts.
A Shared Kernel is another choice when contexts intentionally co-own a small model and can coordinate its changes. That is joint ownership, not a shortcut around language versioning. The earlier comparison shows where that coordination moves.
The fixture breaks each contract without breaking the other
The companion synthetic reproduction retains 24 passing tests, three consumer decisions, independent service and language versions, a blocked v1 retirement, primary-source hashes, and a score-free migration table.
Eighteen isolated counterfactuals try to call any HTTP endpoint an OHS, omit a consumer, publish unnamed semantics, redefine quantity or units inside v1, add a required field, remove a live enum, accept unknown versions, collapse both version axes, hide an authorization break, substitute schema validation for consumer execution, claim an undeployed migration, retire active v1, present a target as current, fork meaning per consumer, remove support ownership, or replace gates with a weighted score. All eighteen fail.
The evidence reproduces from archived commit
208d33377e33b27f8f9ae56f50f1af9a428d3c3d. It proves the declared synthetic
contracts and migration rules. It does not prove production uptime, real
consumer deployment, or that the protocol deserves to be shared.
Before publishing the next API version, write two version decisions. State how consumers obtain the capability. Then state what every value means. If only one of those changed, migrate only the contract which broke.