One module knows their system. Nothing else is allowed to.
The single most useful structural decision an FDE makes: put every fact about the customer's system behind one adapter that returns your own types. It is what lets you develop without their environment, swap a nightly file for an API without a rewrite, and hand over something a stranger can read.
15 MIN
TL;DR: Write one module that knows how their system exposes data, and have it return your own types. Everything downstream consumes your shape, never theirs. This is what makes offline development, a later access upgrade, and the handover all cheap instead of impossible.
Where you are. You can read an unfamiliar system and see its boundaries. This module is about what you actually write against it, and this lesson is the structural decision that everything else in the engagement rests on.
The pattern has a name and it is worth carrying into design reviews, where "let us put a seam here" sounds like a preference and the real name sounds like a decision. Domain-driven design calls it an anti-corruption layer: a translation boundary whose job is to stop another system's model leaking into yours. The word corruption is doing real work in that phrase. What leaks is not data, it is vocabulary, and once their field names and their status codes are in your business logic, their model has quietly become yours and you inherit every future change to it.
The shape of the mistake
You get access to their order system on a Tuesday. It returns something like this, and you are on a deadline:
# What their API actually gives you.
{
"ORDER_NBR": " A-4471 ",
"cust_id": "00099421",
"order_dt": "2026-03-14T00:00:00", # no timezone, always midnight
"sts": "9", # undocumented; means "closed"
"ln_items": [{"sku": "X1", "qty": "2"}] # quantities arrive as strings
}
So you write the fast thing. resp["ORDER_NBR"].strip() in the report builder, int(item["qty"]) in the totals function, if o["sts"] == "9" in three places, because each of them was a two-second decision made while solving something else.
Six weeks later that vocabulary is in nineteen files. Then any one of these happens, and all of them eventually do: they grant you the real API and the field is now orderNumber; a second region sends sts as "09"; you want to run your tests on a plane; a new engineer asks what 9 means and nobody knows.
The problem is not the ugly data. Their data is allowed to be ugly, and it is not yours to fix. The problem is that you let their vocabulary escape into your codebase.
The seam
Define the type you wish they had sent you, then convert once, at the edge:
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from enum import Enum
class OrderStatus(Enum):
OPEN = "open"
CLOSED = "closed"
CANCELLED = "cancelled"
@dataclass(frozen=True)
class Order:
"""Our shape. Nothing downstream knows this came from their system."""
number: str
customer_id: str
placed_on: date
status: OrderStatus
quantity: int
The adapter is the only file allowed to mention their field names, and every piece of tribal knowledge about their system lives here as a comment somebody can read:
# Confirmed with their ops lead on 14 March. "9" is a 2019 migration
# artifact meaning closed; "09" appears only from the EU region.
_STATUS = {
"1": OrderStatus.OPEN,
"9": OrderStatus.CLOSED,
"09": OrderStatus.CLOSED,
"X": OrderStatus.CANCELLED,
}
class UnmappedValue(Exception):
"""Their data used a value we have never been told about."""
def to_order(raw: dict) -> Order:
status = _STATUS.get(raw["sts"])
if status is None:
# Refuse to guess. Quarantine beats a silently wrong report.
raise UnmappedValue(f"unknown status {raw['sts']!r}")
return Order(
number=raw["ORDER_NBR"].strip(),
customer_id=raw["cust_id"].lstrip("0"),
placed_on=date.fromisoformat(raw["order_dt"][:10]),
status=status,
quantity=sum(int(li["qty"]) for li in raw["ln_items"]),
)
Four things happened in thirty lines. Their vocabulary stopped at the door. The undocumented 9 is documented once, with a date and a name. An unknown value raises instead of guessing, which is the quarantine discipline from module four expressed as code. And Order is now the thing your logic is written against, so your logic is testable without their system existing.
Why this is the FDE version and not just good practice
Every engineer has been told to decouple. Three things make the seam specifically load-bearing in field work.
You will lose access. Credentials expire, VPNs drop, environments go down for a maintenance window nobody told you about. With a seam you keep working, because the rest of the system only needs Order objects, and those can come from a saved fixture:
def orders_from_fixture(path: str) -> list[Order]:
"""Same output as the live client, from a file recorded on 14 March."""
return [to_order(r) for r in json.loads(Path(path).read_text())]
Record one real response the first day you have access, per module four's discipline, and you own a development environment that no approval can take away.
The access you start with is not the access you end with. Module two of the Engagement course makes the case for starting on whatever integration shape you can get approved this quarter. That advice only survives contact with reality if upgrading from a nightly file to a live API is one new function behind the same seam, rather than a rewrite.
The handover depends on it. A new engineer reading a seam learns their system in one file. A new engineer reading nineteen files with sts == "9" scattered through them learns nothing and changes nothing, which is how you end up still owning a deployment two years later.
Where the boundary goes
One judgment call, since a seam in the wrong place is just extra indirection.
Put it where their model stops and your problem starts. Everything above the line is a fact about their world you must accept: field names, encodings, pagination, the nightly-versus-live decision, their status codes. Everything below is your logic, and it should be readable by somebody who has never seen their system.
The test to apply: if you deleted their system and replaced it with a completely different customer's, how many of your files change? One is correct. Two is acceptable. Nineteen means the seam does not exist.
Do this before moving on
Take the last integration you wrote and grep it for the source system's vocabulary: their exact field names, their status codes, their identifiers. Count the files. Then write the type you wish they had sent you and the single function that produces it. You do not have to refactor anything today. The count is the finding, and it is usually a number that surprises people.
Go deeper
- Testability and dependency injection is the general principle; the seam is its highest-value application in field work.
- Parsing messy data covers the conversions the adapter body ends up full of.
- Data quality is the discipline behind raising on an unmapped value rather than guessing.
- Enterprise ingestion across forty connectors is what this pattern looks like when it has to hold at scale.
- The system nobody owns is the Engagement lesson that assumes you can build a seam on day one.
Key takeaways
- One module knows how their system exposes data; everything downstream consumes your own type.
- Convert at the edge into a type you defined, and raise on values you were never told about rather than guessing.
- The adapter is where tribal knowledge about their system gets written down once, with a date and a name.
- The seam is what buys offline development, a cheap access upgrade, and a handover a stranger can read.
- The test: swap their system for a different customer's, and only one file should change.
Check yourself
Answer before you look. Recalling it is what makes it stick; recognising it does not.
1You are on a deadline and their payload has ugly field names. Why not just read them directly in the three places you need them?
2Their status column contains a value your mapping has never seen. What should the adapter do?
3What is the practical test for whether a seam is in the right place?
Sign in to track which lessons you have finished.
