I run 2Settle, a product with one stubborn goal: make crypto easy to spend on everyday things. Not trading, not wires. The coffee, the subscription, the bill you owe right now, paid with money you already hold.
v1 shipped on a Node stack and taught me what the product actually is. v2 is a ground-up build in Go. I want to share the reasoning, because "we rewrote it in a faster language" is the lazy version of the story and it is not what happened.
Why the language changed
The honest driver was not raw speed. It was the shape of the problem. A payment is not a request you can retry casually. It is real, final, and there is a person standing at a checkout waiting on it. The moment you take everyday spending seriously, your backend is doing many small, independent, time-sensitive things at once, and each one has money attached.
Go's model for concurrency, thousands of things happening at the same time without the code turning into a callback maze, maps almost exactly onto that. In v1 I was constantly working around the runtime to get predictable behavior under load. In v2 the runtime works with me. That is the whole trade.
Three things v1 taught me that shaped v2
What I am not doing this time
Not chasing every chain and token on day one. Not adding features v1 never validated. Not calling it done. It is still building, still being tested, shipped, and tested again.
Happy to answer anything about the Node to Go decision or building payment infrastructure the second time. What I will not get into is anything live-system specific, for obvious reasons.
"Boring and predictable beats clever"—especially when building payment infrastructure. Moving from Node to Go for better concurrency primitives and predictable behavior under load is a super solid move. Great breakdown on why correctness is a core product feature!
"Boring and predictable beats clever"—especially when building payment infrastructure. Moving from Node to Go for better concurrency primitives and predictable behavior under load is a super solid move. Great breakdown on why correctness is a core product feature!
"Boring and predictable beats clever"—especially when handling money. Moving from Node to Go for better concurrency primitives and predictable behavior under load is a great architectural choice for a payments backend. Solid insights on built-in correctness building trust!
Worth designing against explicitly while v2 is cheap: goroutines make it easier to lose money, not harder.
Two traps that look fine in testing. If the ctx you thread into money-moving work descends from the inbound HTTP request, a client hanging up cancels your payment mid-flight — cancelled leg, no recorded outcome, and you don't know whether the provider saw it. And a bare go doThing() that panics takes the process down with whatever it was halfway through. Money work wants a context you own and a supervised worker off a durable queue.
Which is what "boring and predictable" cashes out to: persist intent, perform the side effect, record the outcome — three writes, so a crash leaves a row instead of a gap.
One thing I'd make a v2 feature rather than a v3 chore: reconciliation against the provider's own record. The failure you can't see from inside your own system is the one where you believe you succeeded and the counterparty disagrees.
The "not casually retryable" point resonates from a different domain — I build offline-first mobile, where the client goes dark mid-write and retries are the normal case, not the exception. The lesson that took me longest: the language matters less than making every write carry its own identity, so a retry is provably the same operation rather than a new one. Once the client generates an idempotency key per intent and the server dedupes on it, "did that go through?" stops being a guess — the retry is safe by construction. I learned this the hard way when forked client state produced duplicate operations that were each individually valid and collectively wrong. Does your v2 push idempotency keys all the way to the client, or resolve finality server-side? For payments the client half seems like where the real ambiguity lives.
Rewriting in Go may improve reliability, but the valuable part is that v1 exposed the real domain model before you committed to a cleaner architecture. Payments backends usually fail at state transitions, retries, reconciliation, and observability, not because Node is inherently incapable.
The boring beats clever line is the one I would frame on the wall. The rewrite risk that scares me most is not picking Go over Node. It is ledger drift showing up two months later from an edge case v1 never hit. When I move something that touches money to a new stack, I run the old and new versions side by side on the same webhook stream for a while and diff the ledgers line by line before trusting the new one alone. That kind of trust is invisible until you actually compare outputs instead of assuming the new code behaves the same, and the reconciliation problem the other commenter raised sounds like the real test of whether this rewrite held up.
The transition from Node to Go for a crypto payments platform is a masterclass in aligning runtime behavior with the unforgiving, concurrent nature of real-world capital, proving that architectural rewrites should be driven by the shape of the problem rather than raw benchmark vanity. Acknowledging that an early version of a product is essentially a research project funded by paying users reframes technical debt, turning messy lessons into a blueprint for simpler, more predictable systems where correctness is treated as a core product feature. Yet, while Go makes the synchronous request path cleaner, the real monsters in payment infrastructure still lurk in the async underworld of late webhooks, duplicate payloads, and unexpected blockchain reorgs. Moving forward, two critical questions remain: first, how does v2 deterministically handle a transaction rollback when a network reorg invalidates a payment your system already treated as final? And second, have you fully committed to deriving user balances directly from an append-only event stream, or are you still maintaining a parallel table that risks drifting under high-concurrency pressure?
Moving a payments backend specifically (not just any service) from Node to Go is a meaningful decision given how sensitive that part of the stack is. Was the switch driven more by performance/concurrency needs, or was it more about type safety and fewer runtime surprises in a domain where bugs are expensive?
Hi, Nice to meet you
yeah the schema tax is real. what's kept it manageable for me: treat event shapes like migrations, never delete or rename a field, tag every event with a version, upcast old ones on read so replay always sees the current shape. and past a certain volume full replay gets slow too, periodic balance snapshots plus replay-from-snapshot is what keeps the 5 minute read honest
hi marc, I hope you are well.
the idempotency point below is the right worry for correctness. the one that got me on similar flows was afterward: a support ticket lands and someone needs to know why the balance is what it is. a current-balance table gives you the what, not the why. an append only trail of every state change makes that a 5 minute read instead of a log-diving afternoon
Agreed, and here's the sharp version: the append-only trail has to be the source of the balance, not a log sitting next to it. The moment it's a parallel audit table, it drifts from the balance exactly when you need the two to agree, because whatever bug skipped writing the balance correctly usually skipped or mangled the audit row too. Fold the events to derive the balance and "why is it this" and "what is it" can't disagree by construction.
Event sourcing is heavier up front, and for money that trade usually pays: the 2am job turns from "reconcile the audit log against reality" into "replay the events." The tax you take on is versioning your event schema forever, which never fully goes away.
"Correctness is a product feature" is the line I'd frame on the wall. One push though: the correctness that bites at 2am usually isn't the concurrent request path Go just cleaned up for you. It's the async reconciliation after the payment. A crypto payment is final on-chain, but your backend hears about it through confirmations and webhooks that land late, out of order, or twice, and every so often a reorg unwinds one you'd already treated as done. Go makes the in-flight handling clean; it can't tell you whether your ledger's view of a payment is eventually consistent with the chain's.
When we built commission tracking on Stripe, every money-correctness bug lived in that same async layer: refunds clawing back a payout, a webhook firing twice, events arriving out of order. Never the request handler. So the v2 question I'm most curious about: what's your idempotency and reconciliation model for an event that arrives twice, or never? That's where "boring and predictable" earns its keep.
I'm curious what v2 has forced you to unlearn from v1. Was there a decision you were convinced was fundamental in the first version that you now see as solving the wrong problem?
The v1 to v2 leap you describe is exactly where most solo founders stall — they get stuck refactoring instead of shipping. I ran into this too. What helped was a hard rule: no rewrite until the existing code actively costs more in maintenance than the rewrite would take. Saved months.