Replaying the ledger found money bugs in the live system.
Before trusting my Go ledger, I replayed real orders through it. The mismatches turned out to be bugs in the live Laravel code.
What happened
While moving our backend from Laravel to Go, I rewrote the ledger: the code that moves money between customer wallets, stores, riders and the company when an order is delivered, cancelled or edited. Before letting it write anything, I wanted proof that it does exactly what production does.
The proof turned up something I did not expect. The Go ledger matched production on every delivered order in the last 60 days. The mismatches it did find were not Go bugs. They were bugs in the live Laravel code:
- When a wallet or part-wallet order was cancelled, the money was never returned to the customer's wallet.
- When a QR payment arrived late, after the order had already been settled as cash, the rider was still charged the cash and the company also kept the QR payment.
- A tip paid after delivery was never booked.
- When an order paid by wallet was edited, the wallet charge was not corrected to the new total.
- Wallet and loyalty balances were updated read-then-write with no row lock, so two updates at the same time could lose one of them.
The replay also showed some store wallet balances higher than their transaction rows explain. I have not found the cause of that yet.
How I found it
I wrote a read-only checker that loads real production orders, runs each one through the Go ledger, and compares the result with what Laravel actually wrote, field by field. It never writes. Every difference became a question: is Go wrong, or is production wrong? For each one I traced the Laravel code path until I could explain the gap.
The fix
Money code does not go straight to production. I built the five fixes on our test environment first, each marked with a tag so a script could take out exactly those changes if needed, and proved each one there. For the late QR case I chose to move the rider's cash amount to digital, with a cap.
Then I applied the patch to production with a backup, confirmed that every patched spot was present, and checked that the API was healthy. The patch changed behaviour from that point on and did not move any past money.
Past cases were a different question. Several affected customers had already been refunded by hand by our team, so an automatic backfill would have paid some people twice, which would have been a new bug of its own. I did not run one; any remaining cases stay with our team to settle by hand, one by one.
What changed
The Go ledger now runs every money movement inside one transaction, locks wallet rows with SELECT ... FOR UPDATE in a fixed order, and records an idempotency key so a retried request cannot apply twice. Because Laravel's behaviour changed, I listed the new plans the Go ledger needs so both stacks keep agreeing.
The bigger change is how I treat a rewrite. I used to think of the replay as a test for the new code. It turned out to be the best audit the old code ever had. Now every piece of the Go migration goes through a shadow or diff check against real data before it can reach production, and every mismatch gets explained, not just fixed.
Get new posts by email
One email when something new goes up. No tracking pixels, unsubscribe in one click.