The second one tells you the shape

High-risk merchants get dropped by processors, so the platform cannot be welded to one. The interface that makes a processor swappable could not be designed before the second one existed.

4 min readGoldVault · Processor stage

A platform serving high-risk merchants has a problem an ordinary one does not. Processors drop this category. Not for anything the platform did, and usually with notice measured in weeks. If the code is welded to one processor, that notice starts an outage that lasts as long as a rewrite.

So the charge goes out through an interface rather than to a particular processor. There are two implementations behind it today and a third slot named but empty. Which one a given charge uses is a row in a table: active processors per merchant, a priority so one is primary and one is fallback, a weight so traffic can be split, and a per-customer override on top for when a single account needs pinning somewhere specific.

It was not designed first

The honest part of this story is the order it happened in. The first integration ran without any abstraction at all for about forty days. The interface, both implementations, the routing table and the generic endpoints all arrived in a single commit on the day the second processor was written. The first processor's code was not rewritten to fit the interface; a wrapper was fitted around it, and the design note of the day says so plainly.

That is the opposite of how this is usually taught, and I think it is correct. An interface designed against one implementation is a description of that implementation. You cannot see which parts are the domain and which parts are one vendor's habits until a second vendor disagrees with the first. The second one tells you the shape.

What disagreed

Almost everything. One takes the buyer to a hosted page and returns a URL; the other returns a token to mount inside the page. One pushes payouts to a hosted flow, the other initiates them server to server with no screen at all. One keeps real customer objects, the other has no such concept, so the adapter hands back the platform's own user id in that slot. One talks in cents, the other in dollars, so the adapter divides on the way out and multiplies on the way back. One sends an event with a type on it; the other sends a settled transaction record with numeric codes and no type at all, so where the first adapter reads a name the second matches a pair of codes.

What survived into the interface is small: set up a customer, start a deposit, start a payout, verify a webhook, parse a webhook. Five operations. Results are normalised to one closed set of outcomes, and anything that will not map is carried in a metadata bag rather than guessed at. The interface does not pretend the two are the same underneath. It publishes the difference as data, a field saying which checkout shape this processor uses, so the code that has to care can ask rather than guess.

What leaked anyway

An abstraction is honest only if you say where it failed, and this one failed in three places worth knowing.

The parameterised webhook endpoint, one URL per processor per merchant, is the centrepiece of the design and one of the two processors cannot use it, because that provider appends its own path segment and will not accept a per-merchant URL. It has a second, hardcoded route that re-implements verification, deduplication and crediting. The generic path exists and serves one of the two.

Disputes escaped the interface entirely. One processor reports them as events through the webhook. The other has no webhook for them, so they arrive by a polling job on a schedule, with its own cursor and its own credentials. The interface models neither, because there is no shape the two share.

And the customer identifier, a single string on the interface, is doing a second job for one processor that it was not designed for, which is marked with a note in the code saying as much. One string was the right call for the common case and the wrong call for the thing that came later.

What it bought

The interface and the routing table have not been revised once since the day they were written. Every commit after that landed in an adapter or in one of the escape paths above. That is the actual measure of an abstraction: not that nothing changed, but that the changes stayed at the edges and the middle held.

The sharpest lesson was not architectural. The second adapter's webhook handling was written from the provider's documentation, and when the first real delivery arrived in the sandbox, every assumption in it was wrong: the signature scheme, the header the signature arrives in, the shape of the payload, and the way a delivery is tied back to the checkout that started it. All four were replaced. Written from documentation, it would have rejected or dropped every deposit. Nothing in an interface protects you from that. Only a real delivery does.

Why it belongs on this page

Deciding to abstract a processor is a one-line decision anyone can make. Knowing which five operations survive, which differences belong in the open as data, and which three things will not fit no matter how the interface is drawn, comes from having integrated the first one badly enough to learn where it hurt. One person wrote the first integration, felt the forty days without an abstraction, wrote the second, and drew the line between them. That is why the line is in a defensible place.

More notes
  • The webhook that credits twiceA payment processor delivers every result at least once. The interesting engineering is in what happens when your side fails halfway through.
  • A ledger you can edit from a route handler is not a ledgerWhy every movement of value on the payments platform goes through a Postgres function, and what it costs to keep it that way.
  • Four rows that were not theirsHardening multi-tenant row-level security in four reversible phases, verified by impersonating a member and counting what they could see.
  • The bug inside the fixA payout race, the fix for it, the bug inside that fix, and why it is the best argument I have for one person owning the whole path.
  • The sale that arrives with no referrerA creator shares a link on Instagram, the buyer taps it, installs the app and purchases. Nothing in that chain carries the creator's name across. Here is what does.
  • The minimum that belongs to someone elseHeld commissions are released to creators once a brand's payout minimum is met. Group the money by creator, the obvious way, and one brand's minimum ends up holding another brand's money.
  • The commission that must not mint twiceA buyer pays and the platform owes a creator a commission. Between those two facts sit a colluding pair, a call that arrives twice, and a cart with three items on one payment.
  • The rule that has to be written twiceFirestore security rules do not cascade to subcollections. Forget that in one place and a single query returns every private message on the platform.
  • Thirty days in the ledgerSplitting a payment at charge time is simpler and wrong: a refund after the creator is paid is a clawback nobody enjoys. Holding the money creates a different set of problems, and each piece of machinery around the hold answers one of them.
  • Arbitrary but consistentA gym's assessment answers become rules that swap an exercise for a member before a session. Two rules can disagree about the same movement. The code says who wins, and the comment admits how.
  • The number that ducked every thresholdA deposit arrives from a browser carrying two numbers. The platform scored one of them and charged the other, and every limit it had was measuring the wrong figure.