Picture a small kitchen run by one chef. Every order — appetizer, main, dessert — flows through the same pair of hands, in the same room, with one shared set of pans. It's fast to set up and easy to reason about, because everything is right there. But as the restaurant gets busy, that single kitchen becomes the limit: one chef can only move so fast, and a spill on the dessert station holds up the steaks too.
Software architecture faces the same tension. Do you keep the whole application in one place where everything is close at hand, or split it into independent stations that can work — and fail — on their own?
The problem
Most systems start as a monolith: a single codebase that compiles and ships as one deployable unit. This is a genuinely good starting point — there's one thing to build, one thing to test, one thing to deploy, and calls between modules are just fast in-process function calls. For a small team and a young product, that simplicity is a feature, not a flaw.
The trouble shows up as the monolith grows. A one-line change to the billing logic still means rebuilding and redeploying the entire application, so releases get slow and scary. Many teams committing to the same codebase start stepping on each other, and merge conflicts and coupling pile up. Worst of all, if one feature — say, search — gets hammered with traffic, you can't scale just that part: you have to run more copies of the whole app, paying to scale everything to relieve pressure on one slice.
- MonolithOne deployable unit holding every feature — a change anywhere means redeploying the whole thing.
How it works
The microservices approach decomposes the system into a set of small, independently deployable services. Each one owns a single business capability — orders, payments, inventory, notifications — along with its own database, and no other service is allowed to reach into that data directly. Services talk to each other only across well-defined boundaries: synchronous APIs for request/response, or asynchronous messaging and events when they can react in the background instead of waiting.
Clients don't call each service individually. Requests come in through an API gateway that acts as the single front door, routing each call to the service that owns that capability. The animation below shows clients hitting the gateway, which fans requests out to several small services — each holding its own database.
- ServiceA small, independently deployable service owning one capability and its own data.
Monolith first. A well-structured monolith — clean modules, clear internal boundaries — is the right default for almost every new system. It lets you learn where the real seams in your domain are before you turn them into network boundaries that are expensive to move later. You can always carve out services once those seams prove stable.
What you gain
The benefits of microservices are mostly about independence. Because each service deploys on its own, a team can ship a fix to payments at 2pm without coordinating a release of the entire system — independent deploys. Because each service runs as its own process, you can give the hot ones more capacity and leave the quiet ones alone — independent scaling instead of scaling the whole app.
There's an organizational payoff too: small teams can own a service end to end and move at their own pace — team autonomy. And isolating services gives you fault isolation: if the recommendations service falls over, the bulkhead between services means checkout can keep taking orders, rather than the whole app going down together.
What it costs
None of that is free — you're trading the simplicity of one process for the realities of a distributed system. Calls that used to be in-memory function calls are now network calls that can be slow, fail, or time out, and you have to design for that. Because each service owns its own data, there's no single database to query across, so you live with eventual consistency and reach for patterns like sagas to coordinate a transaction that spans several services.
The operational tax is real: you need an API gateway, service discovery, distributed tracing, centralized logging, and a deployment pipeline per service. Debugging a request that hops through five services is far harder than stepping through one stack trace. A handful of services is manageable; dozens are a serious investment in tooling and discipline.
Don't adopt microservices for the hype. Splitting a system you don't yet understand into services usually produces a distributed monolith — all the operational pain of microservices with none of the independence, because the services are still tightly coupled and have to deploy together. Microservices solve organizational and scaling problems; if you don't have those problems, they mostly add cost.
When to use it
Reach for microservices when the pressure is real: you have multiple teams whose release cadences are colliding in one codebase, or specific parts of the system have wildly different scaling and reliability needs that a single deployable unit can't serve well. Those are the problems independent deploys, independent scaling, and fault isolation are designed to solve.
Until then, prefer a well-structured monolith and keep its module boundaries clean, so the seams are easy to extract later. The honest guidance is to let team size and scale — not fashion — pull you across the line, and to split one painful boundary at a time rather than rewriting everything into services at once.