
Why MVPs Break Under Real Load
Success is the failure mode. The shortcuts that got you to launch are exactly the ones that break when launch works.

Every monolith migration proposal starts the same way: we'll build the new system alongside the old one, then switch over. Almost none of them end that way. The old system keeps changing because the business keeps needing things, the new system chases a moving target, and eighteen months later you are maintaining two of everything.
The alternative is less satisfying and works far more often: never have a second system. Cut one seam at a time out of the one you have.
Microservices trade a code problem for an operations problem. You get independent deployment and scaling; you pay in network calls, distributed transactions, eventual consistency and a much harder debugging story.
That trade is worth it when you have a specific, named pain:
Put a routing layer in front of the monolith — a proxy, a gateway, or just a reverse proxy rule. Every request goes through it. At first it forwards everything to the monolith and nothing has changed.
Then extract one capability. The new service handles a narrow set of routes; the router sends those to it and everything else to the monolith. Repeat. The monolith shrinks a little at a time, and at no point is there a big-bang cutover to get wrong.
The key property: you can stop at any point and still have a working system. Half-migrated is a stable state, not a crisis. That is the entire advantage over the rewrite.
Not the most painful one. The first extraction is where you learn your deployment story, your observability story and your inter-service auth story — so pick something forgiving:
Notifications are the canonical first service for exactly these reasons. It is dull, and that is the point — extract something dull while you are still learning, not your billing engine.
Splitting code is a week. Splitting a database is the project. The moment two services share a table, you do not have two services — you have one system with extra steps and no transaction.
The sequence that works:
Expect eventual consistency and design for it explicitly. If the business genuinely cannot tolerate a few seconds of staleness somewhere, that is a strong signal the seam belongs inside one service, not across two.
For a lot of products the right answer is not microservices at all — it is a well-structured monolith with the two genuinely independent things pulled out: the workload that needs to scale differently, and the workload that needs a different runtime.
Three services that map to real operational boundaries will serve you better than fifteen that map to an org chart. You can always cut another seam later. That is the whole point of doing it this way.
Use the strangler pattern: put a routing layer in front of the monolith, then extract one capability at a time into its own service and route just those requests to it. The monolith shrinks incrementally and the system stays working throughout, so there is never a big-bang cutover — and you can stop at any point with a stable result.
Something dull and forgiving: low traffic, few dependencies, clear inputs and outputs, ideally asynchronous. Notifications, PDF generation or scheduled reports are ideal. The first extraction is where you work out deployment, observability and service auth, so you want mistakes to be cheap — not your billing engine.
Only if you have a specific named pain: a component that must scale independently, teams blocking each other on deploys, a component needing a different runtime, or a failure that takes everything down. "Our codebase is messy" is not a reason — distributing tangled code just adds network latency and partial failure to the same problem.
The database, without exception. Splitting code takes a week; splitting data is the project. Two services sharing a table is one system with extra steps and no transaction. Give the new service its own store, run both paths in parallel while comparing results, and only then remove the old access.