← All notes
Product & Platform Engineering8 min

Building payments you can swap, and why we carry a second provider

BuilderHelp runs Stripe — including ACH via Financial Connections — and still carries a second working payments path. The hedge is commercial more than technical: terms, underwriting, and vertical risk classification can change unilaterally. An abstraction you never exercised is a claim; a second path you can run is a fact.

The risk is commercial, not technical

The argument for provider portability is usually made on technical grounds — outages, feature gaps, latency. Those are real but they are not the main thing. The main thing is that a payments provider can change your business terms unilaterally and your only response is compliance.

Pricing changes. Underwriting standards change. A vertical gets reclassified as higher risk and suddenly a category of your customers cannot be onboarded, or holds get placed on funds that used to settle. Construction payments involve large amounts to subcontractors on irregular schedules, which is exactly the profile that attracts risk-team attention. None of that is a criticism of any specific provider; it is the structure of the relationship.

If the answer to any of those events is a six-month migration, you do not have a negotiating position and you do not have a contingency. You have a dependency that is allowed to reprice you at will.

What we actually run

Stripe is the primary and gets full use of what it does well, including ACH through Financial Connections, which matters here because card fees on a subcontractor payment are not a rounding error. Bank-to-bank movement is the default for large amounts, and the account-linking flow is materially better than asking a framing contractor to key in routing numbers.

The second provider is not a stub or a config flag pointing at nothing. It is a path that works, exercised enough to know it works, carried alongside the primary. The distinction between 'we designed for portability' and 'we have a second working path' is the entire value of the exercise. The first is a claim about your architecture. The second is a fact you can check.

This is the part teams skip. An abstraction layer written against one provider is shaped by that provider's assumptions, and you find out which ones at the worst possible moment. A layer with two implementations has been forced to be honest.

What a swappable integration buys
Coupled to one provider
  • Provider logic at every call site
  • An outage is an incident with no lever
  • A pricing change is a migration
  • Their roadmap becomes your roadmap
Behind your own interface
  • One boundary the rest of the app talks to
  • A second path already carried
  • Switching is config, not a rewrite
  • Their roadmap is their problem

The question isn't how likely a provider failure is. It's whether it would be an incident or a migration.

Where the seam goes

The seam is drawn around intent rather than mechanism. The application asks to collect a payment of an amount from a payer for a project, or to pay out to a subcontractor. It does not know about the provider's object model, its status vocabulary, or the specific sequence of confirmations a given flow requires.

What leaks in practice is timing and status. Providers disagree about when a bank payment is considered settled versus pending versus available, and those distinctions are visible in the product because a builder cares whether a payment has cleared. So the domain owns its own status vocabulary and each provider adapter maps into it, rather than the product rendering whatever string came back from an API.

The other thing that must not leak is the identifier. Application records key on our own payment identity, with provider references stored as attributes of that record. Keying core tables on a provider's identifier is the single most common way a swappable integration turns out not to be, because the foreign key ends up in twenty places by year two.

What it costs

Being straight about the price: every payments feature is more expensive. A new flow has to be expressed in the domain vocabulary rather than the provider's, which is slower than calling the API the way the docs show. The second path needs maintenance whether or not anyone uses it, and an unexercised alternative decays into a false sense of safety.

There is also a real temptation cost. Providers ship convenient features that assume you are all-in — prebuilt components, hosted flows, dashboard-driven configuration. Some of those we have declined, and declining a genuinely good feature to preserve optionality is a trade that feels bad every time.

We are also not claiming this is free insurance that has already paid off. It has not been called on. The claim is narrower: the cost is a known, bounded engineering tax, and the risk it hedges is unbounded and outside our control.

When to skip it

This is not universal advice. If payments are incidental to your product — a subscription charge, a single price point, small amounts — the abstraction is overhead with nothing behind it. Take the provider's happy path, use their components, and move on.

The threshold is roughly whether payments are a feature or the substrate. When money movement is what customers are paying you to coordinate, when the amounts are large enough that fee structure affects your pricing, and when your vertical could plausibly be re-scoped by a risk team, the calculus changes. Construction payments hit all three.

The middle position, if you are unsure, is cheaper than it looks: keep provider identifiers out of your primary keys and define your own status vocabulary from day one. Those two choices cost almost nothing at the start and are the expensive part of any migration later. You can defer building the second adapter. You cannot easily undo a schema keyed on someone else's identifiers. The test we apply is whether a provider outage or a pricing change would be an incident or a migration. If swapping means touching every call site, you have coupled your business to a vendor's roadmap without ever deciding to.

Questions

Why keep a second payments provider if Stripe is the primary?
Construction payments are large, irregular, and attractive to risk teams. If pricing, holds, or onboarding rules change, a six-month migration is not a contingency. A maintained alternate path is bounded engineering cost against an unbounded commercial dependency.
Where should the payments seam sit in application code?
Around intent — collect or pay out for a project — not around a provider's object model. Own your payment identity and status vocabulary; store provider references as attributes. Status timing (pending vs settled vs available) is what usually leaks otherwise.
When is payment-provider portability not worth it?
When payments are incidental — a simple subscription or small single price point. Use the provider's happy path. At minimum, still keep provider IDs out of primary keys and define your own status words from day one; those two choices make a later migration cheaper.

Sources

  1. Stripe Financial Connections
  2. Stripe — ACH Direct Debit
  3. BuilderHelp case study
  4. Stripe Connect overviewRelevant when money moves between multiple parties.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us