
Somewhere along the way, microservices became the default answer to "how should we build this," and monoliths became something to apologize for. That is backwards. Microservices solve real problems, but they are a solution to organizational and scaling problems, not a starting point. Splitting a monolith into microservices before you have those problems does not make your system more professional, it makes it a distributed system, with all the network calls, data consistency puzzles, and operational overhead that entails, and none of the benefits you were promised. Most teams that split early spend the next year missing the simple monolith they gave up. This guide takes the honest position: keep your monolith until it genuinely hurts, and this is how to know when that is and what to do about it.
To be clear, this is not anti-microservices. It is about sequencing. The right question is never "monolith or microservices," it is "what problem am I solving, and is splitting the cheapest way to solve it." We will look at what splitting actually costs, the modular monolith that gives you most of the benefits with far less pain, the real signals that you have outgrown a single deployable, and a safe, incremental way to extract services when the time comes. The goal is to spend your complexity budget only where it buys something.

The diagram above shows what you actually sign up for when you move to microservices. Each service tends to own its own database, which multiplies what you have to run, back up, and keep consistent. Every call that used to be an in-process function is now a network call, which adds latency and brand-new failure modes, a request can now fail halfway. Debugging a single user action means tracing it across several services, so you need distributed tracing just to see what happened.
You need an API gateway and the platform to run it, infrastructure a monolith never required. And you are now on call for many services instead of one. For a small team, that operational weight is not a footnote, it is most of your week. None of this makes microservices wrong, but all of it is cost you pay up front, before you have earned any of the upside.

Here is the option most teams skip, and it is usually the right one. A modular monolith, shown above, is a single deployable application with strict internal boundaries. Each module owns its own domain and data, and modules talk to each other only through defined interfaces, never by reaching into each other's tables.
You get clean separation, clear ownership, and code that is easy to reason about, but you deploy one thing, debug in one process, and run one database. Crucially, a well-built modular monolith is also the ideal launchpad if you ever do split, because those module boundaries become your future service boundaries.
The discipline lives in the structure and the interfaces. Keep modules from depending on each other's internals.
src/
auth/ # owns users, sessions; exposes AuthModule
orders/ # owns carts, orders; exposes OrdersModule
billing/ # owns invoices, charges; exposes BillingModule
catalog/ # owns products, stock; exposes CatalogModule
shared/ # truly shared primitives only
# Orders talks to Billing ONLY through its public interface,
# never by importing Billing's models or querying billing tables.
class OrdersService:
def __init__(self, billing: BillingModule):
self.billing = billing
def place_order(self, cart):
order = self.repo.create(cart)
self.billing.charge(order.id, order.total) # a clean, swappable seam
return order
That one rule, cross-module communication through interfaces only, is what keeps a monolith from rotting into a "big ball of mud," and it is what makes a later split a refactor rather than a rewrite.

So when is splitting into microservices worth it? The chart above captures the intuition: a monolith is cheaper until a crossover point, after which its costs climb and microservices, despite high fixed overhead, pull ahead. You are near that crossover when you see concrete, recurring pain, not hypothetical future scale. The honest signals are: multiple teams are stepping on each other in one codebase and deploys have become a coordination bottleneck; one part of the system needs to scale independently of the rest, for very different load; different components genuinely need different runtimes or resource profiles; or one flaky component keeps taking down everything and you need fault isolation. Notice that every one of these is an organizational or scaling problem that a single deployable makes worse.
If you cannot point to one of them happening now, you are not at the crossover yet, and splitting will cost you more than it returns.
Just as important is what is not a reason to split: it is not because microservices are modern, not because a big company you admire uses them, and not because your monolith feels messy, since a messy monolith usually just needs the module boundaries from the previous section, not a network between its parts.

When you do have a real reason, do not attempt a big-bang rewrite, which is how these projects fail. Use the strangler pattern, shown above. Put a router in front of your system, then extract one service at a time, starting with the single most painful piece. Route just that path to the new service and leave everything else in the monolith, running and untouched.
# The router sends one path to the new service and everything else to the monolith.
location /orders/ {
proxy_pass http://orders-service; # newly extracted, scales on its own
}
location / {
proxy_pass http://monolith; # everything else, unchanged
}
Because you built clean module boundaries earlier, the extraction is mostly mechanical: the new Orders service exposes the same interface the OrdersModule did, takes ownership of the orders data, and the monolith calls it over the network instead of in process. Ship that, watch it in production, and only then decide whether to extract the next piece. Repeat until the services you actually need exist, and stop there. You are allowed to end up with a monolith plus two services, that is often the correct architecture, not a failure to finish.
The whole decision comes down to a simple discipline: split into microservices only to solve a problem you can name today, and reach for the cheapest tool that solves it. Start with a well-structured modular monolith, because it gives you clean boundaries and clear ownership without the distributed tax, and it doubles as the perfect starting point for a later split. Watch for the real signals, team contention, independent scaling, fault isolation, and when one genuinely appears, extract one service at a time with the strangler pattern, guided by the boundaries you already drew. Microservices are a powerful tool once you have earned them. Until then, the boring monolith is quietly winning, and that is a good place to be.
If you're considering a distributed architecture, read about our microservices mistake before you commit.
If you're just getting started, here's how to set up Claude Code and ship your first task.
Learn how to turn those repeat prompts into reusable Claude Code skills that your whole team can run the same way every time.
If missed calls are costing you customers, an AI voice agent that answers every call can pick up the slack.
Turning a flaky prototype into production-ready AI agents takes idempotency, validation, guardrails, and full observability.
Learn how simple demand forecasting can help you cut food waste and stock closer to real demand.
If this maps to a problem in your business, tell me about it. I will tell you honestly whether software or AI can fix it, and how I would build it.