Anticorruption Layer, Open Host Service, and Published Language
What This Concept Is
Three translation patterns sit on top of the six relationship patterns from concept 4. They answer: whose model crosses the wire, and who translates?
- Anticorruption Layer (ACL) -- a translation layer built by the downstream to convert the upstream's model into the downstream's language. Protects the downstream context from foreign concepts that do not fit.
- Open Host Service (OHS) -- the upstream decouples its implementation model from a public integration model. Protects consumers from internal change.
- Published Language -- the name of the integration-specific model that an OHS exposes. A clean, consumer-friendly contract that is not the upstream's internal ubiquitous language. Often a public schema (JSON schema, AsyncAPI,
.proto).
Where they sit on the power map:
| Who translates? | Power | Pattern |
|---|---|---|
| Upstream translates (for its consumers) | consumer-friendly supplier | Open Host Service + Published Language |
| Downstream translates (to protect itself) | dominant upstream, unwilling consumer | Anticorruption Layer |
| Neither translates (direct model adoption) | dominant upstream, willing consumer | Conformist (no translation) |
Why It Matters Here
Translation decisions are where domain leakage happens. If you let a messy upstream model cross unchanged into your core, your core is contaminated. If an upstream exposes its internal model as its contract, every refactor breaks consumers. The three patterns give each side a clean answer:
- upstream publishes OHS -> internal model can change freely
- downstream builds ACL -> upstream model can be messy, downstream stays clean
- published language -> both sides communicate in a third, contract-shaped language
Concrete Example
Case: Parcel Shipping Co. -- three translations, three patterns
1. Open Host Service + Published Language: Carrier Gateway to downstream contexts
The Carrier Gateway integrates 12 carriers. Internally its code has to deal with FedEx's nested SOAP, UPS's XML dialect, DHL's JSON that disagrees with its own docs, and a carrier whose "success" status is a string "0".
The Carrier Gateway does not expose any of that. It publishes a clean integration model:
// Published Language for "parcel scan event" (AsyncAPI 3.0 payload)
{
"schema_version": "carrier.scan.v1",
"awb": "784199288734",
"carrier": "fedex",
"occurred_at": "2026-04-15T14:32:00Z",
"location": { "city": "Memphis", "iso_country": "US" },
"raw_code": "OC",
"unified_status": "IN_TRANSIT",
"description": "Origin scan"
}
Two properties make this a published language, not an exposed internal model:
unified_statusis an enum that exists only for consumers; no carrier uses this name internallyraw_codeis carried through for debugging, but consumers are told "do not switch on this"
Inside the gateway, the code looks nothing like this payload. The gateway chose this schema to make consumers' lives easy.
2. Anticorruption Layer: Pricing consuming a legacy "contract engine"
Pricing has to honor customer-specific contracts. A legacy system ("ContractEngine v1") is the system of record for those contracts. Its API returns responses like:
<cOntrAct>
<cst_num>12345</cst_num>
<rules>
<rule typ="CAPPED_SVC" svc_cd="G" cap_amt_cents="2500"/>
<rule typ="PCT_OFF" svc_cd="P" pct="0.12"/>
<!-- 30 more rule types, some documented, some not -->
</rules>
</cOntrAct>
Pricing is a core subdomain. Letting ContractEngine's XML, abbreviations, and inconsistent casing into the core would corrupt it. Pricing builds an ACL:
class ContractEngineACL:
"""Translate ContractEngine v1 payloads into Pricing's domain language."""
def load_contract(self, customer_id: CustomerId) -> Contract:
raw = self._http.fetch(f"/cntr?cust={customer_id}")
parsed = self._parse_xml(raw)
rules = [self._translate_rule(r) for r in parsed.rules]
return Contract(customer_id=customer_id, rules=rules)
def _translate_rule(self, raw) -> ContractRule:
match raw.typ:
case "CAPPED_SVC":
return CappedServiceRule(
service=ServiceClass.from_code(raw.svc_cd),
cap=Money.cents(raw.cap_amt_cents),
)
case "PCT_OFF":
return PercentOffRule(
service=ServiceClass.from_code(raw.svc_cd),
percent=Percent(raw.pct),
)
# ...
Inside the Pricing core, nobody mentions cOntrAct, cst_num, CAPPED_SVC, or svc_cd. The ACL is the only place those words exist.
Benefits:
- when ContractEngine v2 ships with yet another rule shape, only the ACL changes
- the Pricing core tests do not depend on ContractEngine at all (the ACL is mocked)
- a new Pricing engineer never has to learn ContractEngine's naming to work on core logic
3. Combined: Tracking's public API is an OHS with a Published Language, and Tracking also has an ACL in front of Carrier Gateway (optional)
Tracking publishes:
# OHS Published Language for Tracking's public API
/shipments/{awb}:
get:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/TrackingStatus"
components:
schemas:
TrackingStatus:
type: object
properties:
awb: { type: string }
status: { type: string, enum: [booked, in_transit, out_for_delivery, delivered, exception, returned] }
last_event_at: { type: string, format: date-time }
journey: { type: array, items: { $ref: "#/components/schemas/JourneyStep" } }
Internally Tracking stores much more (raw carrier scans, dedupe keys, carrier-specific event codes). The OHS hides all of that.
If Tracking decided the Carrier Gateway's carrier.scan.v1 message is also confusing for its internal model (say, the unified_status enum does not match Tracking's needs), Tracking could add a thin ACL at the consumer side. It is legitimate to have ACL on a line that already flows through an OHS.
Common Confusion / Misconception
"ACL is the same as an adapter class." It is the same code pattern, but DDD uses the term specifically for translating domain models, not for low-level adapters like HTTP clients. An adapter hides protocol. An ACL hides a model.
"OHS means exposing REST." It does not. OHS is orthogonal to protocol. Your OHS could be gRPC, GraphQL, a message topic, or a database view. The point is the integration model is decoupled from the internal model.
"Published Language means 'we wrote an OpenAPI spec.'" A published language is an explicit agreement shaped for consumers. A spec is usually the artifact that carries it. An OpenAPI spec that mirrors your internal entities one-to-one is a published implementation model, not a published language.
"ACLs and OHS are mutually exclusive." They are not. A mature integration often has both: the upstream publishes an OHS in one shape, and a particularly picky downstream builds its own ACL in front of that OHS because the OHS still does not fit its model. This is common and healthy.
"Every external integration needs an ACL." Only if the upstream's model would corrupt your core. For generic supporting integrations (emailing SendGrid) a thin client is enough -- you do not have a core model to protect from "email sent."
"The ACL lives in the upstream." No. The ACL is the downstream's defense. If the upstream cleans up its own model for consumers, that is an OHS, not an ACL.
How To Use It
Per edge:
- Identify the upstream and downstream.
- Ask: does the downstream have a core subdomain to protect? If yes, it likely needs an ACL.
- Ask: does the upstream serve multiple consumers and want freedom to refactor internals? If yes, it should expose an OHS.
- If both apply, you will often see OHS on the upstream side and ACL on the downstream side -- that is fine, the layers do different jobs.
- For the OHS, write down the published language: schema, vocabulary, intentional enum values. Review it as a product, not as a side effect of code.
- For the ACL, keep it thin: one translation function per inbound message type. No business logic in the ACL.
Check Yourself
- What is the difference between an OHS and "just a REST API"?
- Why is it usually wrong to add business logic inside an ACL?
- A team exposes a REST API that returns exactly its internal ORM entities. Consumers report breakage on every sprint. What pattern would fix the problem?
Mini Drill or Application
Take three edges from a real system you have access to (or your Parcel Shipping sketch). For each:
- Name the relationship (partnership, customer-supplier, conformist, etc.).
- State whether the upstream currently has an OHS + Published Language (and if so, show the schema names).
- State whether the downstream currently has or needs an ACL, and what it would translate.
- Identify one edge where the published language is actually just the internal model and propose a minimal fix.