Skip to content
Software Survivor logo
Published on

Adapter Boundaries That Make Change Cheaper

Adapter Boundaries That Make Change Cheaper architecture illustration
Authors
  • avatar
    Name
    Antonio Perez
    Twitter

An adapter is valuable only when it changes the cost of a future decision. Renaming a provider SDK does not accomplish that. A good adapter contains vendor semantics, makes business semantics explicit, and gives the next engineer one place to understand the difference.

The practical question is not “should we use the adapter pattern?” It is: “which provider details would be expensive or dangerous if they spread through this workflow?”

For payments, those details include provider transaction states, idempotency behavior, asynchronous settlement, error classifications, and reconciliation identifiers. For persistence, they include query behavior, identifier formats, transaction semantics, and nullability. For an ERP, they include record forms, account configuration, rate limits, and the distinction between a rejected write and an inconclusive response.

Design the contract around a business operation

Start with a caller's decision. A checkout workflow needs to authorize a payment; it does not need to manipulate a provider's raw request object. An order workflow needs to find or create the business record identified by a stable key; it does not need to know an ERP's generated ID format.

The contract should preserve the outcomes that matter:

type AuthorizationResult =
  | { status: 'approved'; providerReference: string }
  | { status: 'declined'; reason: string }
  | { status: 'indeterminate'; providerReference?: string }

interface PaymentCapability {
  authorize(input: AuthorizationRequest): Promise<AuthorizationResult>
}

indeterminate is not an implementation nuisance. It is a business condition with a different recovery path from a decline. A timeout after a write may require lookup or reconciliation before another authorization is attempted. Hiding that distinction behind throw new Error() makes duplicate financial effects more likely.

Keep translation at the edge

The adapter owns request construction, response mapping, provider-specific authentication, and error classification. The application owns the business workflow, durable state, policy, and operator experience.

That separation lets an order workflow stay readable:

validate order -> claim operation -> submit through adapter -> persist evidence -> reconcile if needed

instead of becoming a mixture of provider payload fields, database calls, and retry branches. It also means a test can exercise the workflow against a fake capability without impersonating an entire vendor SDK.

The boundary should retain the original provider identifiers and payload evidence where support, finance, or replay needs them. Containment is not deletion. It means the rest of the system does not need those details to make ordinary business decisions.

Do not force false equivalence

The common failure mode is a lowest-common-denominator interface. If Provider A supports separate authorization and capture while Provider B only settles immediately, an interface that pretends both behave the same will produce surprises in checkout, fulfillment, and financial reconciliation.

There are three honest choices:

  1. Make the difference part of the capability contract.
  2. Support only the workflow that both providers can perform safely.
  3. Reject the unsupported workflow clearly and route it through a different business process.

What does not work is letting the distinction leak out accidentally after the business already depends on it.

An adapter is not a migration plan

The MongoDB-to-PostgreSQL migration case study is a useful constraint on adapter enthusiasm. Repository contracts reduced the number of callers that needed to change, but they did not move historical data, prove record equivalence, synchronize live writes, or provide a safe rollback. Those required checkpointed data movement, reconciliation, and a controlled cutover.

The same is true for commerce and ERP integrations. A clean interface does not resolve duplicate webhook delivery, ambiguous creates, missing mappings, or operational recovery. The order-sync architecture pairs the boundary with idempotent processing, explicit states, and reconciliation because production correctness extends beyond one API call.

Keep the seam proportionate

An adapter should be as small as the decision it protects. Avoid a universal “integration layer” with generic verbs, any payloads, and configuration that tries to represent every future vendor. That framework becomes its own opaque dependency.

Prefer a small contract when:

  • A dependency is revenue, customer, or operations critical.
  • Replacement, dual-running, or multi-provider behavior is plausible.
  • Provider terminology would make business workflows harder to understand.
  • Failure behavior differs enough to require an explicit policy.

Prefer direct code when a dependency is local, short-lived, or has no meaningful replacement path. The second implementation is often the right time to introduce the boundary.

The test for a useful boundary

A useful adapter gives a future engineer three answers without searching across the repository:

  • What business operation is this dependency responsible for?
  • Which provider-specific behavior is intentionally contained here?
  • How are success, failure, and uncertainty represented to the caller?

If the answers are clear, the boundary lowers the cost of change. If the adapter only repeats a vendor API with different names, it has added indirection without leverage.

Continue exploring

Continue with the principles, implementation stories, and consulting paths that apply to the same platform problem.

Working through a similar platform decision?

Bring the business capability, constraints, and failure modes. I can help identify the smallest responsible next step.

Discuss Your Platform Challenge