On this page
Key takeaways
- Keep one system as the book of record for each fact. Adapters translate; they do not compete.
- Adapters fail — design retries, dead-letter handling, and make failures visible to the same people who handle exceptions.
- Model an integration as an explicit boundary with a contract, not a set of scripts.
- If the adapter cannot be turned off without breaking core work, it has grown too large.
Northframe does not pretend to replace the TMS, ERP, or CRM. It sits between them so the people on the floor have one place that matches how work actually happens after hours. That promise is only as strong as the adapters that connect it to the systems you already run.
One book of record per fact
The first rule of adapter design is ownership. For every fact — a load, an invoice, a customer — exactly one system is the book of record. Everyone else holds a copy or a reference.
| Fact | Book of record | Northframe's role |
|---|---|---|
| Load and shipment data | The TMS | Reads; references by id |
| Invoice and ledger | Finance / ERP | Flags holds and approvals; exports outcomes |
| Exception, owner, reopen reason | Northframe | Owns it completely |
| Customer contact | CRM | Reads; does not edit |
The exception is what Northframe owns because nobody else does. That is why it can be the spine of the product without competing with anything.
The shape of an adapter
- Read side. Pull the minimum data needed to give an exception context.
- Write side. Push outcomes back — a hold, an approval — through the system's supported interface.
- Mapping. A visible translation between the external system's model and ours.
- Health. A status anyone on the floor can read.
interface SystemAdapter<TExternal, TInternal> {
name: string;
// Read: fetch and translate, never mutate the external system here.
fetch(ref: string): Promise<TExternal>;
toInternal(external: TExternal): TInternal;
// Write: explicit, idempotent, and reported.
push(outcome: { ref: string; key: string; payload: unknown }): Promise<PushResult>;
health(): Promise<{ ok: boolean; lastSuccess?: string; detail?: string }>;
}
type PushResult = { status: "applied" | "duplicate" | "failed"; detail?: string };Make failures visible where exceptions live
Adapters fail: credentials expire, an upstream is down, a field changes shape. The instinct is to log the error and move on. In an operations product that is a betrayal — the night desk assumes the record is right.
In release 1.2, TMS adapter retries became visible in the same queue as human exceptions. A failed sync is just another exception with an owner and a reason.
| Adapter failure | Poor handling | Better handling |
|---|---|---|
| Upstream unavailable | Silent retry loop | Visible exception with retry state |
| Credential expired | Sync stops quietly | Health turns red; owner notified |
| Unexpected field shape | Record dropped | Quarantined with the raw payload |
| Write rejected | Log line | Exception assigned to a person |
Retries, idempotency, dead letters
- Retry transient failures with backoff and a ceiling.
- Make writes idempotent, so a retry cannot double-apply an approval.
- After the ceiling, move the item to a dead-letter state a human can see.
- Record every attempt, so the morning review can read what happened overnight.
Overgrown adapter
- Caches and reinterprets so much that the copy is trusted over the source
- Core work stops when it is down
- Failures only appear in logs
Healthy adapter
- Reads narrowly and writes sparingly
- Can be switched off without stopping the floor
- Failures show up in the same queue as exceptions
The off-switch test
A useful check on scope: can the adapter be turned off without stopping core work? If the night desk can still capture, assign, and close exceptions with the TMS adapter down, the boundary is healthy. If not, the adapter has grown into the product.
Symptom of overreach
Adapters that cache and reinterpret so much data that the team starts trusting the copy over the source. At that point you are building a second system of record — and inheriting all of its problems.
Meridian's version
The same principle applies in Meridian: a single export into the accounting tool the firm already runs. Approved extras can become a draft invoice, but the ledger stays where finance already trusts it.
Adapter review
- Each fact has exactly one book of record.
- Writes are idempotent and every attempt is recorded.
- Failures become visible exceptions with an owner.
- There is a health status anyone on the floor can read.
- The adapter can be turned off without stopping core work.
The anti-corruption layer idea
Domain-driven design gives a name to what a good adapter does: an anti-corruption layer [1]. It translates between your model and an external one so that the external system's quirks never leak into your core. Microsoft's architecture guidance describes the same pattern for integrating with legacy systems [2]. In an operations product the payoff is concrete: when the TMS renames a field or changes a status vocabulary, exactly one file changes.
| Concern | Inside the adapter | Inside the core |
|---|---|---|
| External field names and formats | Yes — translated at the edge | Never seen |
| External status vocabulary | Mapped to internal states | Internal states only |
| Authentication to the external system | Yes | No |
| Retry, timeout, and rate limiting | Yes | No |
| Business rules | No | Yes |
const statusMap: Record<string, InternalState> = {
"OPEN": "unassigned",
"IN_PROGRESS": "assigned",
"PENDING_CUST": "waiting",
"RESOLVED": "closed",
};
export function toInternal(external: TmsException): InternalException {
const state = statusMap[external.status];
if (!state) throw new UnmappedStatus(external.status); // fail loudly and quarantine, do not guess
return { sourceRef: external.id, state, dueAt: new Date(external.window_end) };
}Contracts you can test
Adapters break silently when the other side changes. The defence is to write down what you expect from the external system and test against that contract continuously. Consumer-driven contract testing [3] does this: the adapter's expectations are recorded as a contract that can be verified against the provider, so a breaking change is caught before deployment rather than at 2am.
- Record real responses as fixtures. Strip secrets, keep shapes, and version them beside the tests.
- Test the unhappy shapes. Missing optional fields, unexpected enum values, empty lists, and oversized payloads.
- Run a scheduled canary. A read-only call against the real system, on a timer, that alerts when the shape changes.
Hyrum's Law [4] explains why this matters in both directions: with enough users, every observable behaviour of an API will be depended on by someone. Your adapter is one of those users, and it will notice when behaviour that was never promised changes.
Timeouts, circuit breakers, and not taking the floor down
An external system that becomes slow is often worse than one that is down, because slow calls tie up your own resources. Every call an adapter makes needs a timeout, a bounded retry policy [5], and a way to stop calling a failing dependency.
| Mechanism | Purpose | Setting to think about |
|---|---|---|
| Timeout | Never wait forever | Shorter than the user's patience, longer than a healthy p99 |
| Retry with backoff and jitter | Absorb transient failures | Cap attempts; retry only idempotent calls |
| Circuit breaker [6] | Stop calling a dependency that is failing | Open after N failures; probe periodically before closing |
| Bulkhead | Isolate one dependency's trouble | Separate worker pools per external system |
| Dead-letter state | Keep failed work visible | Show it in the same queue as human exceptions |
Keeping copies honest: resync and drift
Any adapter that caches or mirrors data will eventually drift from its source. The question is not whether, but how quickly you notice and how cheaply you repair it.
- 01
Store the source reference and last-synced time
Every mirrored record knows where it came from and when it was last confirmed.
- 02
Compare periodically
A scheduled job samples records and compares them to the source, recording the difference rate.
- 03
Prefer re-reading to patching
When a record looks wrong, refetch it from the source rather than editing your copy.
- 04
Provide a manual resync
An operator can trigger a resync for one record, with an audit entry.
- 05
Alert on drift, not on every difference
A single mismatch is noise; a rising rate is a signal.
Anti-patterns
How adapters grow into a second system
- Business rules embedded in mapping code
- Silent fallbacks that guess at unknown values
- Writing to the external system without an idempotency key
- One shared retry loop for every integration
- No visible health, so failures are found by users
How they stay humble
- Rules in the core, translation only at the edge
- Unknown values fail loudly and are quarantined
- Idempotent writes with recorded attempts
- Per-integration timeouts and breakers
- A health status the floor can read
Adapters are the least glamorous part of an operations product and the part that decides whether people trust it. Build them as if they will be read by the engineer who inherits them at 2am, because they will be.
Closing
Good adapters are humble. They read carefully, write sparingly, fail loudly, and can be switched off. Do that, and the product earns the right to sit between systems people already depend on.
References & further reading
- 1Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans, Addison-WesleyAnti-corruption layers are discussed in the strategic design chapters.
- 2Anti-corruption Layer pattern — Microsoft Azure Architecture Center
- 3Pact documentation: consumer-driven contract testing — Pact Foundation
- 4Hyrum's Law — Hyrum Wright, hyrumslaw.com
- 5Timeouts, retries and backoff with jitter — Marc Brooker, Amazon Builders' Library
- 6Bliki: Circuit Breaker — Martin Fowler, martinfowler.com
The product behind this post
Northframe
The operations layer for teams who still run the night shift.
From $890 per month
About the author
Product behaviour described here reflects what is implemented and tested; anything else is marked as planned. Code samples are illustrative.
All writing