Hotshard
Design forks

Monolith vs Microservices

The packaging decision: ship the app as one unit, or as many independent ones?

Every application is packaged one of two ways, and the choice shapes how you build, deploy, and scale it for years. A monolith is one deployable unit: one codebase, one process, one deploy, where the parts call each other in-process β€” a plain function call inside the same program. Microservices split that same application into many small services that each deploy on their own and talk to each other over the network. The monolith is simpler and faster to build; microservices buy you independent deploys and independent scaling at a real operational cost. That single difference β€” parts calling each other in-process versus over the network β€” cascades into how transactions work, how failures spread, how teams ship, and how you debug. This isn't a contest with a winner. It's a fork decided by how many teams you have and what actually needs to scale on its own. This page puts the two side by side on the things that really decide it, and links into the topics you need to make the split work.

~13 min read

Start here: one deploy, or many#

TL;DRthe 30-second version
  • A monolith is one deployable unit: one codebase, one process, one deploy. Its parts call each other in-process β€” a plain function call inside the same program. Microservices split that into many services that each deploy on their own and talk over the network.
  • The monolith is simpler and faster to build. An in-process call is nearly free, a database transaction across features is trivial, and there's one thing to run, test, and debug.
  • The monolith's limit is that it scales and ships as a single unit. Every team shares one release, one hot feature drags the whole app's scaling, and one bad bug can take everything down.
  • Microservices buy independent deploy, independent scaling, team ownership, and fault isolation. You pay for it with network calls, distributed transactions, and real operational overhead β€” so they mostly solve an organizational and scale problem, not a code-quality one.

Point at a running web application and ask how its parts are packaged. There are two shapes. A monolith puts everything into one program: the user code, the order code, the payment code, the notifications code all live in one codebase and compile into one deployable unit. You run copies of that single unit behind a load balancer. When the checkout code needs the user code, it just calls a function β€” the two run in the same process, share the same memory, and share the same database. Microservices take that same application and cut it into separate services β€” a user service, an order service, a payment service β€” each with its own codebase, its own deploy, and often its own database, all talking to each other over the network.

The fork is how one part talks to another. In a monolith it's an in-process call: an ordinary function call inside the same program. It costs nanoseconds, and it either returns a value or throws an error β€” nothing in between. In microservices it's a network call: one service sends a request to another over the network, usually over HTTP or gRPC (gRPC is a fast binary way for services to call each other, covered in its own topic at /grpc). A network call costs milliseconds, not nanoseconds, and it has a third outcome the function call never had β€” it can hang or vanish. In-process versus over-the-network is the entire disagreement, and everything else on this page is a consequence of it.

Why the boundary decides everythingIn a monolith, a call between two features is a function call: fast, reliable, and it can join the same database transaction, so either both changes commit or neither does. Split those two features across the network and every one of those guarantees weakens. The call is now slow enough to matter. It can fail on its own while both services stay up β€” a partial failure. And the two databases can't share one transaction, so keeping them consistent becomes your job. You didn't just move code apart. You turned local guarantees into distributed problems.

What each one actually is#

A monolith is one program. All the features live in one codebase and compile into one deployable unit, and you run copies of that single unit behind a load balancer. Inside it, a call from one feature to another is an ordinary function call, so passing data around is free and instant. There's one database, so a single transaction can update the order table and the inventory table together, and the database guarantees they both commit or both roll back. That all-or-nothing guarantee is what ACID means: a transaction either fully happens or doesn't happen at all. To change anything, you edit the one codebase, run the one test suite, and ship the one artifact.

Microservices take that same application and cut it along feature lines into separate services. The order service and the payment service are now two programs, deployed independently, each often with its own database that only it is allowed to touch. When the order service needs the payment service, it makes a network request. Each service can be written in a different language (this freedom is called polyglot), scaled to a different number of copies, and released on its own schedule without redeploying the rest. To make that work, a service first has to find the others: it asks a service registry 'where is the payment service right now?' and gets back a current address. That lookup is service discovery, and it has its own topic at /service-discovery.

The network is the new function callEvery arrow that used to be a function call is now a request across a network you don't control. That network adds latency β€” the round trip takes real time β€” and it adds failure modes a function call never had. The request can be lost, the other service can be down, or, the nasty case, it can be slow, leaving the caller waiting with no answer at all. Designing microservices is mostly about surviving those calls: putting a timeout, a retry, and a fallback on every hop. The clean, well-defined request-and-response shape each service exposes is its API, and getting those contracts right is its own discipline (/api-design).

What the split actually costs#

The costs of splitting aren't a vague 'complexity.' They're specific, and they're the vocabulary an interviewer expects. Here are the concrete things a monolith gives you for free that microservices make you build yourself.

  • Latency and partial failure on every call. An in-process call was instant and either worked or threw. A network call takes milliseconds and can also just hang. Chain five services to serve one request and their latencies and failure odds stack up.
  • Distributed transactions. A monolith updates two tables in one database transaction β€” all or nothing, for free. Two services with two databases can't share a transaction, so you stitch consistency together yourself: a saga (a sequence of local steps, each with an undo step to run if a later one fails) or the outbox pattern (write the change and its outgoing message in one local transaction so the message can never be lost). Both have their own topics (/saga, /outbox).
  • Debugging across the wire. In a monolith, a stack trace shows the whole path of a request. Across services, one request touches many machines, so you need distributed tracing β€” tagging each request with an ID and following it across every service β€” just to answer 'where did this slow down?'
  • Data consistency across databases. When each service owns its own data, there's no single source of truth to join against. The same customer's details may live in three services, and keeping those copies agreeing is an ongoing design problem, not a given.
  • Operations. Many services means many things to deploy, monitor, secure, and keep discoverable. You now need service discovery, health checks, per-service dashboards, and a pipeline that can ship dozens of units β€” real, permanent overhead a single deploy never had.

There's also a failure mode with a name: the distributed monolith. This is what you get when you pay all the costs of microservices β€” the network calls, the separate deploys, the distributed transactions β€” but draw the boundaries wrong, so the services stay so tightly coupled you can't actually deploy one without the others. You took on distributed-systems difficulty and kept the monolith's coupling. It's the worst of both worlds, and it's exactly what a bad split produces.

MonolithMicroservices
Call between partsIn-process function call (nanoseconds, reliable)Network call (milliseconds, can fail or hang)
Transaction across featuresOne local ACID transaction, for freeDistributed β€” saga / outbox, built by you
DeployOne unit; all teams share the releasePer service, independent
ScalingWhole app scales as one unitPer service β€” scale only the hot one
A crashCan take the whole app downIsolated to one service (if boundaries hold)
Debugging one requestA single stack traceDistributed tracing across machines
PredictA single request to your app fans out to 5 microservices in sequence, and each service is up 99.9% of the time. Roughly how often does the whole request succeed β€” and what did the monolith give you here for free?

Hint: Independent 99.9% services in a chain β€” multiply the success probabilities.

Multiply the odds: 0.999 to the fifth power is about 0.995, so roughly 1 request in 200 fails somewhere in the chain, even though every single service is a healthy 'three nines' 99.9%. The more hops you add, the worse it compounds β€” ten services in a chain drops you to about 99%, or 1 in 100. In a monolith those five steps were in-process function calls that don't independently fail, so the request succeeded or failed as one unit and the 99.9%-per-hop tax simply didn't exist. This is why microservices lean so hard on retries, timeouts, and fallbacks: without them, availability quietly erodes with every service you add to the path. The one-line takeaway: every network hop is another chance to fail, and those chances multiply.

The trade, and the axes that decide it#

Neither side wins in the abstract. The monolith trades independence for simplicity; microservices trade simplicity for independence. What tells you which trade to take is a short list of axes β€” look at your situation on each one.

  • Number of teams. One or a few teams sharing one codebase is fine β€” they coordinate easily. Many teams all committing to one repo and blocking on one release queue is where the monolith hurts, and where independent deploys start to pay for themselves.
  • Independent scaling of a hot component. If one part β€” say video encoding β€” needs ten times the machines of everything else, splitting it out lets you scale just that part. If everything scales together fine, there's nothing to buy here.
  • Fault isolation. If one flaky feature must never take down the core product, a separate service contains its failures. In a monolith, a memory leak or crash in any feature can sink the whole process.
  • Transactional coupling. If two features must change together atomically all the time, keeping them in one service β€” one database, one transaction β€” is far simpler than a saga. Splitting tightly-transactional features is how you buy distributed-transaction pain.
  • Operational maturity. Microservices assume you already have deployment automation, monitoring, tracing, and on-call in place. Without that foundation, a fleet of services is just more things breaking with no way to see why.
Conway's law: the org shapes the architectureThere's an old observation called Conway's law: a system's structure ends up mirroring the communication structure of the organization that built it. Three teams tend to produce three services whether they meant to or not. The practical reading is that microservices are as much an org decision as a technical one. If your boundaries between services don't match how your teams are actually split, you'll fight the architecture constantly. This is why microservices are said to solve an organizational problem β€” letting many teams ship independently β€” more than a code-quality one. Well-organized code doesn't require the network; many teams that want to deploy without waiting on each other do.

There's a middle option that's often the right default: the modular monolith. It's still one deployable unit β€” one process, one deploy, in-process calls, local transactions β€” but the code inside is split into clear modules with enforced boundaries, as if each module were a future service. You get the monolith's simplicity now and clean seams to extract a real service later if a specific pressure demands it. The pragmatic path most teams should follow is: start with a modular monolith, and extract a service only when a concrete need actually shows up β€” a team that must deploy independently, or a component that must scale on its own. Extracting under real pressure beats splitting on day one for a scale you don't have yet.

PredictA 6-engineer startup is designing a new product and reaches for microservices from day one 'so it'll scale later.' What's the strong pushback, and what would you do instead?

Hint: How many teams need to deploy independently at six engineers β€” and what do you pay to split before you know the boundaries?

The pushback: microservices solve problems this team doesn't have yet, while charging them costs they'll feel immediately. With six engineers there's one team, not many, so there's no independent-deploy or team-autonomy pressure β€” which is the main thing microservices buy. What they'd get instead is distributed transactions, network-failure handling, service discovery, and tracing to run, all before they even have product-market fit, and most likely a distributed monolith, because the right boundaries aren't knowable this early β€” you don't yet know which parts change together. The strong move is to start with a modular monolith: one deploy, in-process calls, local transactions, but clean internal module boundaries so a service can be cleanly extracted later. Then extract under real pressure β€” when a specific component needs independent scaling, or the team grows into several that block on one release. This is the mainstream, hard-won default (often called 'monolith first'): you keep early speed and buy the split only when a concrete need pays for it. The one-liner: don't take on a distributed system to solve an org problem you don't have yet.

The decision, in one table#

Put the whole decision on one table. Read down the left for your situation; the lean is a sensible default, not a law.

If this describes you…Lean towardBecause
One or a few teams, one productMonolith (modular)No independent-deploy pressure; simplicity wins
Many teams blocked on one release queueMicroservicesIndependent deploy lets teams ship without coordinating
One component needs very different scalingExtract that serviceScale the hot part alone, leave the rest as one unit
Features are tightly transactionalMonolithOne local ACID transaction beats a hand-built saga
A flaky feature must not sink the coreExtract that serviceA separate process contains its failures
No deploy automation / monitoring / on-call yetMonolithMicroservices assume that foundation already exists
Early-stage, boundaries still unknownMonolith firstSplit later under real pressure, not on a guess

Notice that most rows don't say 'microservices everywhere.' The common real-world shape isn't a pure monolith or a hundred services β€” it's a monolith with a few high-value services carved off where a specific pressure justified each one. Split at the seams that hurt, and keep the rest together. When in doubt, the burden of proof is on splitting, not on staying together.

In the wild
  • Amazon and Netflix are the canonical microservices stories β€” but both started as monoliths and split only once they had hundreds of engineers and scale pressure that made independent deploys and per-service scaling worth the cost. The split followed the org growth; it didn't precede it.
  • Shopify runs one of the largest Ruby on Rails codebases in the world as a modular monolith β€” deliberately one deploy, with strictly enforced internal module boundaries β€” as a public argument that you can scale a monolith a very long way with good internal structure.
  • Segment moved from a monolith to microservices and then back to a monolith, because the operational overhead of a large service fleet outweighed the benefits for their team size. It's a well-known reminder that the split isn't a one-way ratchet.
  • 'Monolith first' is the mainstream default advocated by practitioners like Martin Fowler: begin with a monolith, learn where the real boundaries are, and extract services only when a concrete need appears β€” because splitting up front tends to produce a distributed monolith with the wrong seams.
  • Uber, Twitter, and others have written publicly about microservice sprawl β€” hundreds or thousands of services becoming hard to reason about β€” and pulled back toward larger, coarser services. More services is not automatically better.
Pitfalls & gotchas
Aren't microservices just the modern, better way to build β€” isn't a monolith legacy?

No. A monolith and a set of microservices are two points on a trade, not old versus new. A monolith is simpler, faster to build, and easier to keep consistent; microservices buy independent deploy and scaling at a real operational cost. Plenty of large, modern companies run monoliths on purpose. 'Legacy' really describes code that's hard to change, and a badly-built microservice system can be just as hard to change as any monolith.

Do microservices make my code cleaner?

Not by themselves. Clean code comes from good module boundaries, and you can have those inside a monolith. What microservices force is that the boundaries are physical β€” you literally can't reach across them without a network call β€” which can impose discipline, but it also means a bad boundary now costs you a distributed transaction instead of a quick refactor. Microservices are an org and scaling tool, not a code-quality tool.

What's a distributed monolith, and why is it the worst case?

It's a set of services so tightly coupled that you still can't deploy one without the others. You pay all the costs of the network β€” latency, partial failure, distributed transactions β€” but keep the monolith's coupling, so you get none of the independence you split for. It usually comes from drawing the service boundaries in the wrong place. It's why people say 'get the boundaries right, or don't split.'

If I start with a monolith, haven't I just guaranteed a painful rewrite later?

Only if you build a big tangle with no internal seams. Build a modular monolith β€” clear modules with enforced boundaries β€” and extracting a module into its own service later is a contained job, because the seam already exists. The rewrite pain comes from tangled code, not from having chosen one deploy unit.

QuizA team splits their app into 12 microservices but finds they still have to deploy all 12 together every release, and a single user request now fails intermittently for no clear reason. What most likely happened, and what did they lose?

  1. They didn't use enough services β€” splitting further will fix the coupling.
  2. They built a distributed monolith: the boundaries are wrong, so the services are still coupled, and they've taken on network latency and partial failure while keeping the monolith's lockstep deploys.
  3. Microservices are always like this; nothing is wrong.
  4. The problem is the programming language; rewriting in a faster one will fix it.
Show answer

They built a distributed monolith: the boundaries are wrong, so the services are still coupled, and they've taken on network latency and partial failure while keeping the monolith's lockstep deploys. β€” This is the distributed monolith. Having to deploy all 12 together means the services are still tightly coupled β€” the boundaries were drawn in the wrong place, so nothing can actually ship independently, which was the whole point of splitting. On top of that, the intermittent per-request failures are the new network cost: every call between services can now fail or hang on its own (partial failure), and a request that fans out across many of them compounds those odds. So they lost the monolith's simplicity and its all-or-nothing local behavior, and gained none of the independent deploy they split for. The fix isn't more services β€” it's correcting the boundaries so that things that change together live together, or merging them back until they do.

In an interview

This comes up in almost any design question that touches team structure or scale. The trap is treating it as a fashion choice β€” 'we'll use microservices because that's modern.' The strong answer frames it as a trade and names the axis that decides it.

  • State the fork first: a monolith is one deploy with in-process calls and local transactions; microservices are many independent deploys talking over the network. Everything else follows from that.
  • Name what microservices buy β€” independent deploy, independent scaling, team autonomy, fault isolation, polyglot β€” and what they cost β€” network latency, partial failure, distributed transactions (saga/outbox), service discovery, tracing, and operational overhead.
  • Say the honest framing out loud: microservices mostly solve an organizational and scaling problem, not a code-quality one. That one sentence signals you understand the trade rather than the hype.
  • Reach for the middle: the modular monolith as the default, extracting a service only under a specific pressure β€” a team that must deploy alone, or a component that must scale alone. 'Monolith first' is a defensible, mainstream default.
  • Name the anti-pattern: the distributed monolith β€” all the costs of the network, none of the independence, from bad boundaries. Knowing its name and cause is a senior signal.
PredictAn interviewer says: 'We have one Rails monolith and 40 engineers across 5 teams. Deploys have become a bottleneck β€” every team waits in one release queue, and one team's bug blocks everyone's deploy. Do we go microservices?' What's the strong answer?

Hint: What kind of pain is 'every team waits in one release queue' β€” and does that justify splitting everything, or splitting along team lines?

Yes, but scoped β€” and lead with the reason, not the pattern. The pain here is specifically an independent-deploy problem: five teams are serialized on one release queue and coupled by one shared failure, which is exactly the organizational pressure microservices relieve. So this is a real case for splitting, unlike the six-person startup. But the strong answer resists 'rewrite into 40 services.' It extracts along team boundaries β€” carve out the services each team fully owns so that team can deploy on its own cadence β€” and keeps tightly-transactional, shared code together to avoid needless distributed transactions. It also checks the prerequisite: at 40 engineers they likely have, or must build first, the deploy automation, monitoring, and tracing that microservices assume. The framing that scores: identify that the pain is team-autonomy and deploy-coupling (the axis), match the split to team boundaries so Conway's law works for you rather than against you, and extract incrementally from the monolith instead of a big-bang rewrite. The one-liner: split where the org hurts, not everywhere.

References
References

Feedback on this topic β†’