Blog
The Monolith You Have Beats the Microservices You Can't Operate Yet
Microservices trade local complexity for distributed complexity, so the honest decision is about your team, your operational maturity, and your real scaling pain rather than fashion.
Microservices are sold as the architecture of serious engineering organizations, and the implication is that a monolith is something you eventually grow out of. That framing has cost a lot of teams a lot of time. The truth is less flattering to either side: a monolith and a microservices system are different sets of trade-offs, and the right choice depends on your team size, operational maturity, and the specific problems you are actually facing. Microservices do not remove complexity; they move it from inside your codebase, where a debugger and a stack trace can reach it, out into the network, where it becomes latency, partial failure, and distributed tracing. This article lays out the real costs and benefits of each so you can make the decision on engineering grounds rather than résumé-driven or conference-driven ones, and so you can recognize when you have genuinely outgrown a single deployable.
What A Monolith Gets Right
A monolith is a single deployable application where all your modules live in one codebase and run in one process. This arrangement has real advantages that teams forget once they are mid-migration. Calls between modules are ordinary function calls, so they are fast, type-checked end to end, and either succeed or fail atomically without partial-failure handling. A single database means transactions are trivial: you wrap related writes in one transaction and either all of them commit or none do. Debugging is straightforward because a stack trace spans the whole request, and reproducing a bug rarely requires standing up a dozen services. Deployment is one artifact, and there is no version skew between components because they ship together. For a small or medium team building a product whose shape is still changing, this simplicity is not a limitation to escape, it is leverage that lets you move fast and refactor freely.
Where Monoliths Hurt
Monoliths have genuine pain points, and pretending otherwise is as dishonest as overselling microservices. As a codebase grows, build and test times climb, and a slow feedback loop drags on every change. Deployment becomes higher-stakes because every release ships the entire application, so a risky change in one corner can force you to delay shipping a safe change elsewhere, and a single bad deploy can take everything down. Scaling is coarse-grained: if one feature is CPU-hungry, you scale the whole application rather than just that part, paying for capacity you do not need. Teams step on each other when many people commit to the same codebase, and a tightly coupled monolith can become a tangle where a change in one module breaks a distant one. None of these are automatically fatal, and many are solvable with discipline, but they are the legitimate forces that eventually push teams toward splitting.
What Microservices Buy You
Microservices split your system into independently deployable services, each owning a slice of the domain and usually its own datastore. The headline benefits are organizational as much as technical. Independent deployment means a team can ship its service without coordinating a release across the whole company, which matters enormously once you have many teams. Independent scaling lets you give the heavy service more instances and the light one fewer, optimizing cost and performance per component. Technology heterogeneity becomes possible, so a team can use a different language or datastore where it genuinely fits. Fault isolation can be better, since one service crashing need not take down the rest if you design for it. Crucially, these benefits are about scaling an organization and a system that have already grown large, and they pay off when coordination across a single codebase has become the dominant tax on your velocity.
The Distributed Tax
Every benefit of microservices arrives with a bill. An in-process function call becomes a network call, which can be slow, time out, or fail entirely, so you must design for retries, timeouts, and circuit breakers everywhere. A single database transaction across modules becomes a distributed coordination problem, and you trade atomic transactions for eventual consistency and patterns like sagas that are far harder to reason about. Debugging now spans many services and many log streams, so you need centralized logging and distributed tracing just to follow one request. Local development gets harder because running the system may mean running many services or building elaborate mocks. You inherit a fleet of deployments to operate, monitor, and secure. None of this is insurmountable, but it is real, ongoing work, and a team that has not built the operational muscle for it will spend more time fighting infrastructure than shipping features.
Operational Maturity Required
Microservices presuppose a level of operational capability that many teams underestimate. To run them sanely you need automated deployment pipelines per service, because deploying a dozen services by hand does not scale. You need centralized, searchable logging and distributed tracing so you can follow a request across service boundaries when something goes wrong. You need health checks, metrics, and alerting on every service, and ideally service discovery and a way to manage configuration consistently. You need a culture and tooling for handling partial failure, because in a distributed system something is always degraded somewhere. Often this means a container orchestrator and the expertise to run it. If your team cannot yet deploy and observe a single application well, splitting into many will multiply the gaps rather than solve them. The honest prerequisite question is whether you can operate one service excellently before you commit to operating twenty.
Service Boundaries Decide Everything
The hardest part of microservices is not the plumbing but the boundaries. Split along the wrong seams and you get distributed services that are still tightly coupled, so every feature requires changing several services together and deploying them in lockstep, which gives you all the cost of distribution and none of the independence. Good boundaries follow the domain: each service owns a coherent business capability and its own data, and it talks to others through well-defined contracts rather than reaching into their databases. A useful heuristic is that a well-bounded service can be developed, deployed, and reasoned about largely on its own, and changes usually stay within it. Getting boundaries right requires understanding your domain deeply, which is exactly the understanding you lack early in a product's life. This is the strongest argument for starting with a monolith: it lets the boundaries reveal themselves before you cement them into calls.
The Modular Monolith Middle
There is a middle path that captures much of the upside of both, and it deserves to be the default for more teams. A modular monolith is a single deployable that is internally organized into well-separated modules with explicit interfaces and clear ownership, where modules do not reach into each other's internals or share tables directly. You get monolith simplicity in deployment, debugging, and transactions, while building the clean internal boundaries that make a future split feasible. If a module later becomes a genuine bottleneck, you can extract it into its own service with far less pain because the seam already exists. This approach lets you defer the distributed tax until you have evidence you need to pay it, and it forces the architectural discipline that benefits the codebase regardless of whether you ever split. Many teams that adopt it discover they never need full microservices at all.
Deciding For Your Team
Make the decision on concrete signals, not aspiration. Start with a monolith, ideally a modular one, unless you have specific evidence that points elsewhere. Reach for microservices when you feel real, recurring pain that a single deployable cannot ease: many teams blocked by a shared release cadence, components with wildly different scaling profiles, or sections of the system that genuinely need different technology. Count the cost honestly, including the platform and observability investment and the ongoing operational burden, and weigh it against the productivity you expect to regain. Avoid splitting because a respected company did, because they solved problems at a scale you do not have. The best architecture is the one your team can build, ship, and operate reliably given its size and maturity today, while leaving room to evolve. Premature distribution has killed more roadmaps than monolithic clutter ever has.