What payment providers can learn from Go contexts
GOTO may have been “considered harmful” since 1968. But we’re still using it a lot.
Maybe not on the happy paths of our software. Jumping from one line of code to another arbitrary line, with neither rhyme nor reason, is something engineers don’t do. GOTO, when things go well, is harmful indeed.
But when things don’t go well, GOTO is actually a requirement.
Well, maybe it’s not exactly GOTO. But what would you call a try/except clause? Isn’t the word try a concesion that you aren’t exactly sure if something isn’t going to go according to plan?
Think about it. You call a function foo, expecting a value bar that’s obtained from executing a function baz:
def foo():
bar = baz()
return barBut baz doesn’t work as expected. And what does baz do? Pass some error to bar? No. It raises an exception, and some part of your callstack has to catch it.
Maybe it’s foo. Maybe it’s the function that called foo. Maybe it’s the function that called the function that called foo. Or maybe nobody catches it, and the language throws an error.
It may not be exactly a GOTO. But it looks a lot like one1.
Now, like I said, you and I still use GOTO statements a lot. And the world isn’t quite on fire, so it must be working.
But there are problems, outside the happy path, that are caused precisely by the use of these try/excepts.
The most egregious one has to do with timeouts in payment orchestrators.
Let’s say you’re requesting a payment to Provider A through your Payment Orchestrator (PO). This request is going to take a loooot of time for *reasons*2. And so, your system is waiting for the request to be served, until it hits a timeout of your choice, and then stops.
But your hitting a timeout doesn’t mean anything for PO. Good PO has proxied your request to Provider A, meaning it has made another request to A, and it’s waiting for it to be done in order to serve your request. PO will patiently wait for A’s response for a bit longer than you, until it hits a timeout of their choice, and then will stop and send you a 504 Gateway Timeout error (even though you aren’t listening to it anymore).
But this isn’t where the story ends! Perhaps Provider A couldn’t serve the request on time because it was waiting on the issuer’s system. And, provided they waited for Issuer a bit longer than you, or PO, they may eventually hit a timeout of their choice, and then stop, and send a 504 to PO, who isn’t listening by then.
So what does this have to do with try/excepts, anyway? We’ll see in a moment. But perhaps notice that systems don’t have a direct way to communicate to their callees that they’re no longer waiting for a response.
In other words, the servers are in control of when the connection times out, not the clients.
Which is a problem. Because, in some cases, the Issuer may take a bit longer to respond, but eventually send a successful payment! But Provider A is no longer listening, and neither is PO, or your system, and so the payment is assumed to be failed, even though it succeeded.
This approach is at the root of what make try/except statements tricky: Even though the client has no control over the server’s timeout, they can choose to abandon the communication and GOTO a failed payment, but the request may be eventually served anyway.
Try/excepts are GOTO statements. And GOTO statements are harmful.
In today’s article, I want to explore another way to approach timeouts, one that’s used by the Go programming language and, to my limited knowledge, no other language out there.
If you know Go, you know I’m talking about Contexts. Because Go has no try/except statements3, it has a different mechanism to handle errors. Rather than catching exceptions, contexts are passed as function parameters, and the caller retains the ability to cancel the process.
This has implications beyond writing code. It requires a complete different mindset about error handling: one that’s top down, rather than bottom up.
And it could be the key that finally fixes those nasty scenarios when an Issuer process a payment, but there’s nobody listening anymore.
A Newsletter For The Engineers That Keep Money Moving
Designing payment systems for interviews is easy. Designing them for millions of transactions is not.
After nearly a decade building and maintaining large-scale money software, I’ve seen what works (and what doesn’t) about software that moves money around. In The Payments Engineer Playbook, I share one in-depth article every Wednesday with breakdowns of how money actually moves.
If you’re an engineer or founder who needs to understand money software in depth, join close to 2,000 subscribers from companies like Shopify, Modern Treasury, Coinbase or Flywire to learn how real payment engineers plan, scale, and build money software.
In this article, I’m going to describe how Go context work, and what payment providers can learn from this approach:
What is a context tree, and how is it different from exception handling
What Go gets right about data flow, but most payment providers don’t
How to build systems that are more resilient to partial failure and inconsistent behavior based on Go contexts
What makes me believe this approach is possible
Let’s dive in.
Keep reading with a 7-day free trial
Subscribe to The Payments Engineer Playbook to keep reading this post and get 7 days of free access to the full post archives.


