DEV Community

Daniel Parkson Tano
Daniel Parkson Tano

Posted on AI-assisted

Design Provider Switching Across the Whole Payment Flow

Keeping every step of a payment flow on the same provider without changing the customer journey.

Adding a second payment provider is easy. Switching safely between providers is the real engineering problem.

A payout flow usually includes bank discovery, account validation, recipient storage, fee quotation, execution, status checks, webhooks, and reconciliation. If each layer selects its provider independently, the system can mix incompatible identifiers.

Resolve the provider once and pass the decision through the workflow:

provider = provider_router.resolve(country="NG", operation="payout")
bank = provider.get_bank(request.bank_code)
recipient = provider.resolve_account(bank, request.account_number)
result = provider.transfer(recipient, request.amount)
Enter fullscreen mode Exit fullscreen mode

Provider-specific bank codes should remain provider-specific. Saved recipients should record which directory produced their details. Cached bank lists should also include the provider in their cache key.

The adapter boundary should expose business operations rather than raw HTTP endpoints. That keeps controllers and transaction services independent of provider response shapes.

The customer flow can then remain stable while operations change the underlying rail. A good provider switch should feel uneventful to users because the architectural work happened before the switch.

Define a business-facing provider interface

An adapter should expose capabilities such as list_banks, resolve_account, quote_fee, create_transfer, and get_transfer_status. It should not leak provider endpoint names or response formats into views and serializers.

Each adapter translates its provider's vocabulary into internal types. One provider may call a successful payout SUCCESSFUL; another may use COMPLETED. The rest of the application should receive one canonical status.

Capabilities also need to be explicit. A provider may support payouts but not collections, or support bank transfers in one country and mobile money in another. Routing should evaluate country, operation, currency, environment, and availability rather than relying on one global string.

Treat reference data as provider-owned

Bank codes are a frequent source of subtle failures. Two providers may use different identifiers for the same institution. Matching by a normalized name looks attractive, but names change, contain abbreviations, and are not guaranteed to be unique.

Store the provider alongside each bank and recipient record. Include it in cache keys. When the active provider changes, show recipients created for that provider or require revalidation before reuse. Do not rewrite the saved identifier because operations may later switch back.

Separate configuration from transaction history

Provider selection can change at runtime, but an existing transaction must remember which provider handled it. Copy the provider, rail, fee configuration, and relevant identifiers onto the transaction attempt. Historical transactions must remain explainable after configuration changes.

Design the fallback deliberately

Automatic fallback after an ambiguous timeout can create two payouts. Only retry through another provider when the first attempt is known not to have been accepted. Otherwise keep the transaction processing and reconcile it.

A safe provider abstraction makes switching easy, but it must never make duplicate money movement easy.

Implementation checklist

Inventory every provider-dependent operation before enabling a switch. Include reference-data caches, recipient favorites, scheduled tasks, webhook routes, status normalization, fee rules, staff actions, and finance reporting. Add provider identity to logs and transaction attempts so production behavior can be traced without exposing credentials.

Test each provider as a complete vertical flow rather than testing only its transfer call. Run bank discovery through final reconciliation using the same configuration production will use. Also test switching away and switching back; saved data must remain valid for its original provider.

The abstraction is complete when no downstream layer has to guess which provider owns a piece of data.

Top comments (0)