The first time you integrate a payment provider, the routing logic seems obvious. You pick a provider, hard-code it, and move on. The problem surfaces when you add a second provider, then a third, then a compliance requirement that treats specific corridors differently. At that point you discover that what started as a single provider call has grown into something far more tangled.
This is the trajectory almost every platform follows, and it is predictable enough that we see the same pattern regardless of the platform's industry or transaction volume. Routing logic accumulates in application code because that is where the first payment call was made, and inertia keeps it there long after it should have been extracted.
The Anatomy of the Accumulation Problem
When a second provider is added for redundancy or cost reasons, routing logic starts appearing in the application layer. It might be a function called selectProvider that checks a flag or a customer segment. Then you add region-based routing. Then fee optimization. Then compliance exclusion rules for specific corridors under MAS guidance.
Over 12 to 18 months, that function grows into something that touches your customer database, your fee tables, your compliance flags, and a dozen internal configuration values. The surface area of change for routing logic has expanded across your application codebase. Now when a provider changes its fee structure, or when MAS issues a new circular requiring additional transaction screening for certain corridors, the change requires a full application deployment.
This is the core problem: routing logic living in application code is coupled to your deployment cycle. It also means that your finance and compliance teams cannot act on a routing change without going through engineering.
What Gets Embedded and Why It Matters
The specific things that end up embedded include: provider priority order, fee threshold logic (route to provider B when amount exceeds SGD 50,000), coverage exclusions (provider A does not support certain remittance corridors), compliance screening thresholds, retry strategy per provider, and timeout configuration.
None of these are business logic in the product sense. They are infrastructure decisions about how money moves. When they live in your application code, they carry the same change cost as product features. A product engineer has to test, review, and deploy them. Finance and compliance teams cannot adjust thresholds without engineering involvement. This creates a bottleneck that is invisible until it creates a compliance gap or a missed cost optimization window.
The Runtime Coupling Problem
Beyond maintenance, there is a runtime coupling problem. When routing logic lives inside your application process, a routing failure can affect your application. A bug in the routing function that causes an exception can surface as a 500 to your end users, or silently fall through to a default provider in a way that violates compliance rules.
We encountered a version of this while building Checker. A platform we worked with had embedded provider selection logic in their checkout flow, co-located with order creation logic. A change to provider priority caused an order to be placed against a provider that did not support the transaction type. The error was silent on the surface and only appeared in reconciliation the following morning. The root cause was that routing knowledge was distributed across several services with no single authority for routing decisions.
This is not a code quality problem. It is an architecture problem. Routing logic that is distributed across your application does not have a single point where you can observe, audit, or change it.
What the Routing Layer Should Own
A routing layer should own exactly one thing: given a payment request with amount, currency, recipient details, and a routing strategy preference, return the best provider and the parameters to execute against it.
Everything else is application logic. The routing layer holds the fee tables. The routing layer holds the provider health state. The routing layer holds the compliance rules. Your application holds none of these. When MAS issues a new circular, the routing layer is updated. Your application deploys nothing.
The routing layer should expose a deterministic, narrow API surface. In Checker's case, that is a single call: POST /v1/payments/route with amount, currency, recipient, and strategy. The response includes the provider selection, fee estimate, compliance check result, and a reconciliation handle. Your application receives a decision and executes it. It does not participate in making the decision.
This separation also creates a testability benefit. You can simulate a provider degradation event and verify that routing fails over to your secondary without touching your application test suite. Routing behavior becomes independently verifiable.
The Nuance: Starting With Application Code Is Not Wrong
We are not saying that every platform should have a purpose-built routing layer from day one. For a platform processing under a few thousand transactions per month with a single provider, embedding routing logic in application code is completely reasonable. The cost of extraction is higher than the maintenance burden at that scale.
The problem is that teams rarely extract routing logic when the moment is right. They extract it when the pain is too high: after a production incident, after a compliance audit finding, after a month-end reconciliation failure that traces back to a stale routing rule. At that point, the refactor is expensive because routing logic has grown tendrils into the rest of the application over years of development.
The right trigger for extraction is when you add a second provider, not when you have three years of routing logic to untangle. At the point of adding provider two, the abstraction cost is low and the ongoing maintenance benefit is highest.
What Extraction Looks Like in Practice
If you are extracting routing logic from application code, the key first step is establishing a contract boundary before you move the logic. Define what your routing API surface looks like, then have your application call that surface. For a period, the implementation behind that surface can be a thin wrapper around your existing logic. The important change is that your application no longer directly references provider-specific constants, fee tables, or compliance thresholds.
The extraction sequence is: define the API contract, update call sites to go through the new surface, migrate the routing logic into the new layer, and then clean up the legacy references. Done incrementally over a few weeks, this is a manageable refactor even for a team that carries other product work simultaneously.
For platforms using Checker, the routing layer is external by design. Your application calls POST /v1/payments/route, gets back a routing decision, and executes the payment. Provider selection, fee calculation, health checks, and compliance screening all run outside your application process. When we update routing rules (say, because a provider changed its fee schedule for SGD cross-border transactions), your application does not change.
This is the correct distribution of responsibility in a payment infrastructure stack. Your application owns the business transaction. The infrastructure owns the money movement. Conflating the two is what creates the maintenance spiral that teams eventually have to unwind at significant cost.
The longer routing logic stays in application code, the more expensive extraction becomes. Starting the extraction at the second provider is almost always the better trade than waiting for a compliance audit to force the issue.
Build on Checker
Payment routing, reconciliation, and compliance for Southeast Asian fintech platforms. Start free, scale as you grow.
Get API Key Free