4
17 Comments

Building v2 of my payments backend in Go after a year on Node. Here is what v1 actually taught me.

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

  1. Version one is a research project you get to charge for. You do not know what the product is until real people use it in ways you did not plan. Everything I thought was a core feature on day one looked different once money moved through it.
  2. Boring and predictable beats clever. In payments, the exciting path is the one you never want to hit. v2 is deliberately less clever than v1 in a lot of places, and it is far easier to reason about at 2am.
  3. Correctness is a product feature, not a tax. Users do not see your concurrency model, but they feel it the first time a payment does exactly what they expected, instantly. That trust is the product.

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.

on July 24, 2026
  1. 1

    "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!

  2. 1

    "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!

  3. 1

    "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!

  4. 1

    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.

  5. 1

    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.

  6. 1

    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.

  7. 1

    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.

  8. 1

    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?

  9. 1

    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?

  10. 1

    Hi, Nice to meet you

  11. 1

    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

    1. 1

      hi marc, I hope you are well.

  12. 1

    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

    1. 1

      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.

  13. 1

    "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.

  14. 1

    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?

  15. 1

    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.

Trending on Indie Hackers
I built an AI that turns an idea into a live business in under 10 minutes. Here’s what 1,000 launches taught me User Avatar 89 comments Building a startup costs $0. Your tooling budget costs $500K. Here's why. User Avatar 42 comments Building Noodle, a keyboard-first REST client for the terminal User Avatar 34 comments "Looks Good to Me" Is Quietly Killing Your Feedback Loop User Avatar 33 comments Stop losing deals in the gap between "sounds good" and getting paid User Avatar 31 comments 787 tools for developers. 5 for nurses. Two weeks of tracking 14,000 indie launches. User Avatar 28 comments