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.