Playbook

Provider Abstraction Without Leaking SDKs (Provider Abstraction Without Leaking Sdks)

How the provider gateway owns the PSP SDK while the checkout orchestrator only ever sees a semantic interface — and why card and wallet flows share a contract…

Distributed Payment Engine

Part 9 of 22

A series on distributed payment architecture — the gap between capture and complete.

Distributed payment engine architecture diagram

The previous part drew the line between payment evidence and payment state. This part draws a more fundamental line: should the checkout orchestrator ever see a PSP's SDK at all?

The short answer is no. The orchestrator should know exactly two things: a charge was requested, and a result came back. Which PSP produced that result, with which SDK version, over which HTTP client — that's the provider gateway's job, not the orchestrator's.

Checkout Orchestrator
      │  ChargeRequest (semantic)
      ▼
Provider Gateway
      │  PSP SDK / HTTP client
      ▼
   PSP A or PSP B

This boundary looks trivial until you try to swap a provider three years later — then it's the decision that determines whether that swap takes a day or a quarter.

Where the concepts first show up

📦 Provider Gateway
The single service that owns the PSP SDK, authentication, and provider-specific flows.

📦 Semantic interface
The contract the orchestrator actually sees — it references no provider type, ever.

📦 Anti-Corruption Layer
A translation layer that keeps an external system's model out of your own domain language.

📦 Adapter
Code that turns a provider-specific request into a semantic one, and a provider-specific response back into a semantic result.

'SDK leakage' isn't a style nitpick: the moment a PSP's ChargeObject type appears in orchestrator code, swapping that PSP stops being a file change and becomes a change that reaches into the orchestrator's core.

Why SDK leakage is quiet debt

The fastest way to build a provider gateway is to pass the PSP's SDK objects straight through to the orchestrator: import ChargeResponse, read a field, branch on a status. It's fast in week one. But the moment that type enters the orchestrator's signature, the two services are silently version-coupled: the SDK updates, a field renames, and the orchestrator either fails to compile or — worse — silently misbehaves.

❌ Orchestrator code
if (pspResponse.charges.data[0].outcome.network_status === 'approved') { ... }

✓ Orchestrator code
if (chargeResult.status === ChargeStatus.Captured) { ... }

The second line carries no provider name at all. The gateway knows which PSP field to inspect; the orchestrator only knows the semantic result.

Why card and wallet flows share an interface but not a flow

Card payments are mostly synchronous: a request goes out, and an authorize/decline result comes back within a few hundred milliseconds. Wallet or redirect-based flows (where the customer is sent to the PSP's own page) are asynchronous: the first call only returns a pending status and a redirect URL; the real outcome arrives minutes later via a webhook.

Card flow
  ChargeRequest → [synchronous call] → ChargeResult (Captured/Declined)

Wallet flow
  ChargeRequest → ChargeResult (Pending + redirectUrl)
         ...
  Webhook → ChargeResult (Captured/Failed) [asynchronous, later]

The semantic interface represents both with the same ChargeResult shape; the difference lives in the status field, where Pending exists as a legitimate third state. The orchestrator never asks 'does this PSP use a redirect' — it only asks 'is this result final yet, or still pending'.

What must never cross the boundary

Provider-specific error codes, provider-specific object identifiers (the PSP's own internal charge-id format), and provider-specific metadata shapes must never leak through the semantic interface. The gateway keeps that information in its own logs and diagnostics; the orchestrator only gets back a result keyed by its own correlation id.

Stays inside the gateway (never leaks)
  provider_raw_code, provider_object_id, provider_response_headers

Crosses to the orchestrator (semantic)
  ChargeResult { status, amount, currency, providerRef }

providerRef is the one exception: an opaque reference string kept for support and diagnostics, but the orchestrator never branches on it.

Distinctions that get blurred

❌ Wrapping the SDK in a class is enough
✓ Wrapping doesn't fix type leakage; behavior can still stay provider-specific

❌ Abstraction means defining an interface
✓ Abstraction means deciding what the orchestrator must never know

❌ With one PSP, abstraction is unnecessary
✓ Even with one PSP, abstraction buys testability and mockability

Thin wrapper vs. real abstraction

Criterion Thin wrapper Semantic abstraction
Type leakage Usually present Absent
Orchestrator affected by PSP swap Yes No
Who owns card/wallet differences Orchestrator Gateway
Testability Requires PSP mocks A fake semantic result is enough

Checklist when designing the interface

  1. Does ChargeResult reference any PSP's name, field name, or error code directly?
  2. Can the orchestrator handle a Pending result correctly without knowing whether the flow used a redirect?
  3. Does adding a new PSP require even one line of orchestrator code to change? If so, the abstraction is leaking.
  4. Can the gateway's test double produce every semantic state without ever calling the real PSP?
  5. Does any opaque field besides providerRef reach the orchestrator's decision logic?

Answering these yes/no questions turns 'clean architecture' from a vague debate into a measurable boundary test.

What to take away

  1. The provider gateway is the single owner of the SDK and every provider-specific detail.
  2. The orchestrator only knows intent (ChargeRequest) and semantic outcome (ChargeResult).
  3. Card and wallet flows share the same interface; the difference lives in the Pending status.
  4. Nothing opaque or provider-specific besides providerRef should ever cross the boundary.

The real test of an abstraction is that adding a new provider never touches the orchestrator's code.

The next part moves this boundary to events: should the webhooks the gateway receives reach the orchestrator as raw provider payloads, or as semantic events?

FAQ

Frequently asked questions

What is Provider Gateway?

The single service that owns the PSP SDK, authentication, and provider-specific flows.

What is Semantic interface?

The contract the orchestrator actually sees — it references no provider type, ever.

Is it true that "Wrapping the SDK in a class is enough"?

Wrapping doesn't fix type leakage; behavior can still stay provider-specific

What does this part lock in?

This boundary looks trivial until you try to swap a provider three years later — then it's the decision that determines whether that swap takes a day or a quarter. The provider gateway is the single owner of the SDK and every provider-specific detail. The previous part drew the line between payment evidence and payment state. This part draws a more fundamental line: should the checkout orchestrator ever see a PSP's SDK at all?

Engineering Principles Learned

  • Leaking an SDK type into the orchestrator means a PSP swap touches the whole system.
  • A semantic interface describes intent and outcome, never the provider.
  • Card and wallet share a contract, not a timing.

Continue reading

Continue reading

Next in series

Next in series

Same series

ESSAY

A timeout, a 429, a 5xx, a business decline, and an infrastructure fault are not the same failure. Each category needs its own retry policy.

Paylaş