In April 2025 we took on an engagement with Meridian Ledger — a mid-market payments platform processing around ₹40 crore a day across a monolithic Rails app that had grown over seven years. The team had done a lot right: good test coverage, careful migrations, an on-call rotation that worked. But the monolith had become a bottleneck for feature velocity, and the two-hour Sunday-morning deploy window had become a running joke that nobody found funny anymore.
They wanted services. Not because "microservices" is a magic word, but because the payments processing, the reporting, and the merchant onboarding had genuinely different rhythms — different deploy cadences, different scaling needs, different teams. The monolith was making all three worse.
What follows is the shape of how we did it. Some numbers are rounded and some names changed at the client's request.
The constraint that shaped everything
Meridian's SLA with their banking partners was 99.98% availability, measured monthly. That's about eight and a half minutes of downtime per month. A rewrite could not spend that budget — we needed the migration itself to be invisible to merchants and to the banks.
That constraint drove every technical decision. In particular, it ruled out the two migration patterns most teams reach for first:
- Big-bang rewrite. Obvious no. Even a "we'll write the new one, then flip DNS" plan hides a hundred edge cases that don't show up until real traffic hits.
- Parallel-run and cut over. Better, but still concentrates the risk into one switch. And with payments, an inconsistent state at the switch is worse than a brief outage — you can end up double-crediting merchants.
We went with the third, less glamorous option: strangler-fig, endpoint by endpoint. Nothing new about it. Martin Fowler wrote it up in 2004. What matters is the discipline of applying it in a payments context where correctness has to be provable, not asserted.
The plan, in five sentences
Put a routing layer in front of the monolith. Extract one endpoint at a time into a new service. Route a small percentage of traffic to the new service, compare responses against the monolith in real time, and only cut over when the diff rate is zero for a full week. Repeat for the next endpoint. Never, at any point, have production depend on a change that isn't fully reversible in under sixty seconds.
The routing layer
We built a thin proxy in Go, sitting in front of the monolith. Every incoming request went through it. For each endpoint, we had a rule that said one of three things:
route: monolith # default, unchanged behaviour
route: shadow # send to both, respond with monolith, log diff
route: split(monolith 90, service 10) # partial cutover
route: service # fully cut over
The routing config lived in a small Postgres table, editable at runtime with a rollback command that never took longer than a config reload. In the whole engagement we invoked that rollback five times. Three of them were within the first hour of a cutover, one was after a bad deploy on our side, and one was for a corner case that only appeared on the first business day of a quarter.
The shadow-and-diff step
The most important tool in this migration was not a framework or a database. It was a diffing script. Every endpoint we extracted first ran in shadow mode for at least seven days, during which we recorded every request and compared the monolith's response to the new service's. The diff was structured — we didn't care about ordering of JSON keys, but we cared about numeric equality down to the paise.
The rule was simple: don't route real traffic to a service until the diff rate has been zero for seven consecutive days including a weekend and a business day.
On the first endpoint — /v1/merchants/:id/balance — the diff rate at day one was 3.2%. It took us three weeks of chasing subtle rounding differences, timezone bugs and one delightful case where the monolith was returning a stale cached value, to get to zero. Every fix on the new service was a fix on the monolith too — the whole point of shadow mode is that it teaches you what your existing system actually does, versus what you think it does.
The order we extracted things
We ordered extractions by two axes: read-only before write-heavy, and low-blast-radius before high-blast-radius. In practice that meant:
- Weeks 1–3: Read-only reporting endpoints. Low risk. Let the team get comfortable with the pipeline.
- Weeks 4–8: Merchant profile and configuration. Writes, but not on the money path.
- Weeks 9–14: Settlement calculation. Complex, but easy to shadow because it runs in batches.
- Weeks 15–20: The transaction path itself. The scary one. Left for the end deliberately.
By the time we got to the transaction path, we had shipped the previous four categories to production, the team was fluent in the shadow-and-diff workflow, and we had built up a library of test fixtures generated from real traffic. What could have been a terrifying migration was, at that point, mostly boring. That was the goal.
What went wrong
Not everything did. But a few things worth noting:
We under-invested in observability early. The first two weeks of the shadow-and-diff phase were painful because we didn't have good tooling to slice the diffs by merchant, by endpoint version, by client SDK version. We built the dashboards in week three and should have built them in week zero.
One extraction had to be reversed. A webhook delivery service we'd built as a separate deployment turned out to have subtle ordering guarantees the merchants depended on that weren't documented anywhere. We rolled it back after ninety minutes in production, folded the requirement into the design, and re-shipped it three weeks later. No merchant reported the incident because the rollback kicked in before any of them noticed.
The team was tired by month four. Strangler-fig migrations are a slog. There's no big-bang launch to celebrate — every week is another two endpoints migrated, another three fires put out. We should have scheduled a mid-migration break more assertively than we did.
Where things landed
Twenty-two weeks after we started, the monolith had been reduced to a routing shim. Deploys went from a Sunday-morning event to a normal Tuesday afternoon. The unplanned-downtime numbers, month over month, actually improved during the migration itself, not after — the shadow-and-diff process caught bugs that the monolith had been silently living with for years.
We're still on retainer with Meridian, handling the platform team's on-call load and iterating on the services. The Sunday deploy jokes have been retired.
Filed under: case-study, migrations, fintech · ← Back to journal