The monolith is Perl. No framework I can identify – just a large codebase that’s been growing for years. One Perl line does an alarming amount. I’m not a Perl developer and had to read through the code several times before I was confident I understood what it was doing.

Production goes down for days sometimes. When it does, the team spends hours tracing through the code to figure out what broke. That’s the context. The system works, mostly. When it doesn’t, nobody quite knows why.

A few of us are tasked with carving services out of this monolith and rewriting them in Go. We’d been writing Go at the previous job and brought it along. No fresh deliberation about language choice – Go was already the plan. The rest of the team maintains the running system while we work behind the scenes on the replacements.

Why payment first

The monolith handles everything. Catalog, orders, users, payments, logistics. We need to pick one piece to carve out first. Payment was the obvious choice.

Payment has a clean boundary. A request comes in from the internal system – “charge this amount through this gateway.” The payment service talks to the external payment gateway, gets a response, and updates the internal system with the result. In, out, done. No complex state management, no long-lived transactions, no entanglement with the rest of the order lifecycle.

The other candidates were messier. Orders carry state across multiple steps – created, confirmed, paid, shipped, delivered, returned. Cutting the order service means owning that entire state machine, including all the edge cases the Perl code handles silently. Catalog has deep ties to search and browse. Logistics touches everything.

Payment was the piece we could rewrite, test, and swap in without worrying that we’d broken some invisible dependency three layers away.

The cut

The Go service went up alongside the Perl monolith. Traffic split by percentage – a small fraction to the new service, the rest to Perl. We watched error rates, response times, gateway success rates. When the numbers matched Perl’s for a few days straight, we felt safe enough to push the percentage higher.

We didn’t run both systems in parallel for long. The motivation was to move over, not to maintain two payment paths. Once the new service was stable – or what we thought was stable – we shifted the percentage up quickly and cut over. A few days of watching logs, then Perl stopped handling payments entirely.

We didn’t want to maintain two payment paths any longer than we had to. Cut over, commit to the new system, fix whatever breaks.

Reading someone else’s code

The hardest part of this whole exercise wasn’t writing Go. It was reading Perl. Perl that was written by people who thought in Perl – terse, expressive, doing three things in one line with regex and implicit variables.

I had to read the payment flow end to end, multiple times, to be sure I understood the happy path and the failure modes. What happens when the gateway times out? What happens when the callback comes back with an unexpected status? What happens when the database write after a successful charge fails? The Perl code handled all of these, some explicitly, some by accident – which I only understood after reading the same block three times. The Go rewrite had to handle all of them on purpose.

There’s no shortcut for this. You read it until you understand it, then you write the replacement, then you test it against every scenario you found in the original. The Perl code is the spec. There is no other documentation.

What’s next

Orders and the wallet service. Those are bigger, messier, more entangled. But the approach is the same. Read the Perl until you understand it. Write it in Go. Route a percentage. Watch the numbers. Cut over.