Engineering · 10 Sep 2026

Designing adapters that do not try to own your ERP.

Operations software earns trust by sitting between systems, not replacing them. The adapter layer is where that promise is kept or broken.

Published
10 Sep 2026
Reading time
7 min
Readers
—
Topics
NorthframeIntegrationsArchitectureERP
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.

TMS · ERP · CRMBooks of record stay where they are
Read adapterFetch the minimum, translate
NorthframeOwns exceptions, owners, reasons
Write adapterPush outcomes, idempotently
Back to sourceHolds and approvals applied
Northframe sits between systems — it does not replace them

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.

FactBook of recordNorthframe's role
Load and shipment dataThe TMSReads; references by id
Invoice and ledgerFinance / ERPFlags holds and approvals; exports outcomes
Exception, owner, reopen reasonNorthframeOwns it completely
Customer contactCRMReads; 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.
Illustrative interface — the boundary, not the implementationTypeScript
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 failurePoor handlingBetter handling
Upstream unavailableSilent retry loopVisible exception with retry state
Credential expiredSync stops quietlyHealth turns red; owner notified
Unexpected field shapeRecord droppedQuarantined with the raw payload
Write rejectedLog lineException assigned to a person

Retries, idempotency, dead letters

  1. Retry transient failures with backoff and a ceiling.
  2. Make writes idempotent, so a retry cannot double-apply an approval.
  3. After the ceiling, move the item to a dead-letter state a human can see.
  4. 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.

1Book of record per fact
1.2Release where adapter retries joined the exception queue
1Export into the accounting tool the firm already runs

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.

ConcernInside the adapterInside the core
External field names and formatsYes — translated at the edgeNever seen
External status vocabularyMapped to internal statesInternal states only
Authentication to the external systemYesNo
Retry, timeout, and rate limitingYesNo
Business rulesNoYes
Illustrative — the mapping is data, so it can be reviewed and testedTypeScript
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.

MechanismPurposeSetting to think about
TimeoutNever wait foreverShorter than the user's patience, longer than a healthy p99
Retry with backoff and jitterAbsorb transient failuresCap attempts; retry only idempotent calls
Circuit breaker [6]Stop calling a dependency that is failingOpen after N failures; probe periodically before closing
BulkheadIsolate one dependency's troubleSeparate worker pools per external system
Dead-letter stateKeep failed work visibleShow it in the same queue as human exceptions
ClosedCalls flow normally
FailingErrors accumulate
OpenCalls short-circuited, exception raised
Half-openA probe call is allowed
ClosedRecovered
A circuit breaker, in words

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.

  1. 01

    Store the source reference and last-synced time

    Every mirrored record knows where it came from and when it was last confirmed.

  2. 02

    Compare periodically

    A scheduled job samples records and compares them to the source, recording the difference rate.

  3. 03

    Prefer re-reading to patching

    When a record looks wrong, refetch it from the source rather than editing your copy.

  4. 04

    Provide a manual resync

    An operator can trigger a resync for one record, with an audit entry.

  5. 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

  1. 1
    Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans, Addison-WesleyAnti-corruption layers are discussed in the strategic design chapters.
  2. 2
    Anti-corruption Layer pattern — Microsoft Azure Architecture Center
  3. 3
    Pact documentation: consumer-driven contract testing — Pact Foundation
  4. 4
    Hyrum's Law — Hyrum Wright, hyrumslaw.com
  5. 5
    Timeouts, retries and backoff with jitter — Marc Brooker, Amazon Builders' Library
  6. 6
    Bliki: Circuit Breaker — Martin Fowler, martinfowler.com
Found this useful? Share it

Get the next essay in your inbox

Practical writing on payments infrastructure, operations software, and shipping real systems. No spam, no sales sequence.

We only use your email to send the studio's writing. See the privacy policy.

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

Have a system like this to run?

Explore the catalog, or write down the problem and the constraints. We respond when the fit is real.

Free apps from the studio. Enter your email, get a private download link. Free for personal use.

Get them free

Have a product to sell? We review, list, and sell it for you — you keep 90% of every sale.

Apply to sell with us