Migrating a live monolith without a maintenance window
We moved a telehealth platform off a failing monolith while it was running live consultations. No maintenance window, no big-bang cutover. Here is what we routed first, what we left until last, and the two calls we got wrong.
The situation
A single Rails application, seven years old, handling scheduling, video consultations, prescriptions, billing and clinical records. It fell over reliably at around 300 concurrent consultations. Demand had just gone vertical and the number they needed was closer to ten thousand.
The obvious answer — rewrite it — was also the answer that had already been attempted once and abandoned eighteen months in. Rewrites fail because the old system keeps changing while you build the new one, and because the day you finally cut over is the day you discover everything nobody documented.
Strangler fig, briefly
You put a routing layer in front of the monolith. Every request still goes to the old system by default. One capability at a time, you build a replacement service and flip the router so that path goes to the new code instead. The monolith shrinks until what remains is small enough to retire or leave alone.
The advantage is that every step is independently reversible. If the new scheduling service misbehaves, you change one routing rule and traffic goes back to the monolith in seconds. Nobody has to decide whether to abort a migration at two in the morning.
What we routed first
The instinct is to start with the worst-performing part. That is usually wrong. Start with something that is genuinely self-contained, read-heavy and low-risk, because the first extraction is really about proving the routing layer, the deployment pipeline and the observability — not about the feature.
We started with appointment availability lookups. High traffic, no writes, no clinical data, and a clear correctness test: does the new service return the same slots as the old one? We ran both in parallel for two weeks, comparing responses and alerting on divergence, before sending a single user to the new path.
- First: read-only, high-traffic, easily verified — availability lookups
- Second: the video consultation layer, where the performance problem actually lived
- Third: notifications and scheduling writes
- Last: clinical records and billing, where correctness matters most and traffic is lowest
Shadow traffic is the whole trick
For every extraction, the new service received a copy of production traffic and its responses were compared against the monolith's, with differences logged but discarded. No user was affected. We ran this until the divergence rate was zero for a sustained period.
This caught things no test suite would have. A timezone handling difference that only appeared for one clinic in a region observing a half-hour offset. A rounding behaviour in duration calculation that had been wrong in the monolith for years and which the business had come to depend on.
Shadow traffic found a bug the monolith had been shipping since 2019. The correct fix was to reproduce the bug faithfully in the new service and fix both later, on purpose.
The two things we got wrong
First, we underestimated the shared database. Both systems read the same PostgreSQL instance for months, which was the pragmatic choice, but it meant the new services inherited the monolith's schema decisions. Splitting the data turned out to be harder than splitting the code and we should have started it earlier in parallel rather than treating it as a later phase.
Second, we let the routing layer accumulate logic. It started as a clean proxy and gradually acquired auth checks, header rewriting and a few special cases. By month five it was a small application in its own right, and an undocumented one. We refactored it eventually, but it should never have been allowed to grow that way.
Where it landed
Fourteen months, zero planned downtime, and one twenty-minute incident caused by a misconfigured connection pool rather than the migration itself. The platform now handles around twelve thousand concurrent consultations and infrastructure spend fell by nearly half, because the extracted services scale independently instead of scaling the whole monolith to serve its busiest component.
The monolith still exists. It handles a handful of internal admin functions that nobody has a business reason to move. That is a perfectly good outcome — the goal was never to delete it, it was to stop it being the constraint.
Tell us what you are building
A short call, an honest read on whether we are the right fit, and a clear next step either way. No pitch deck.
Replies within one working day
