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.
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
- Does
ChargeResultreference any PSP's name, field name, or error code directly? - Can the orchestrator handle a
Pendingresult correctly without knowing whether the flow used a redirect? - Does adding a new PSP require even one line of orchestrator code to change? If so, the abstraction is leaking.
- Can the gateway's test double produce every semantic state without ever calling the real PSP?
- Does any opaque field besides
providerRefreach 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
- The provider gateway is the single owner of the SDK and every provider-specific detail.
- The orchestrator only knows intent (
ChargeRequest) and semantic outcome (ChargeResult). - Card and wallet flows share the same interface; the difference lives in the
Pendingstatus. - Nothing opaque or provider-specific besides
providerRefshould 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
Semantic Events Over Raw Provider Payloads
Should the webhook the provider gateway receives reach downstream consumers under the PSP's own event name, or as a semantic event like…
Next in series
Payment Evidence vs. Payment State
Evidence is what the PSP told you. State is what you decided. Conflate the two, and recovery leaves you unsure which one to trust.
Same series
Payment Failure Taxonomy
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.