Part 10 · Architecture & Deployment
Monolith vs Microservices: An Honest Comparison
A five-person startup with 14 services, and a 300-engineer company with one two-hour deploy — both chose wrong. Monoliths, microservices and the modular monolith in between, how to find boundaries, how to migrate with the strangler fig, and the signals that tell you which fits.
On this page
The Problem
A five-person startup reads that Netflix and Uber use microservices (many small services that work together). So they split their brand-new app into 14 services on day one. Six months later:
- adding one feature means changing 5 services, planning 5 deploys (releases to production) and reading 5 sets of logs,
- a simple "show order with customer name" page needs 3 calls over the network,
- working on the app needs 14 containers (small isolated boxes that each run one program) on a laptop,
- nobody remembers which service owns what.
Meanwhile, a 300-engineer company has one giant codebase (all the source code in one place):
- every deploy takes 2 hours and includes 90 people's changes,
- one bug in the reporting code crashes checkout,
- teams constantly block each other.
Both companies chose the wrong architecture for their situation. The monolith vs microservices question is not about which one is "modern". It is about team size, how well you understand the business area (the domain), and how good you are at running software (operational maturity).
The Core Idea
A monolith is like a big restaurant with one kitchen. Everyone cooks in the same room. Chefs can easily pass ingredients around and work together. It is simple to run while the team is small. But with 200 cooks in one kitchen, people bump into each other. One chef's fire alarm sends everyone out. Changing the menu needs the whole kitchen to agree.
Microservices are like a food court. Each stall (pizza, dosa, biryani, desserts) runs on its own, with its own staff, equipment and opening hours. The dosa stall can change its menu or close for repairs without affecting the others. But a customer who wants a combo meal must visit several stalls. And someone has to manage the shared seating, cleaning, payments and teamwork.
Neither is better. The right choice depends on how many cooks you have and how well you know your menu.
How It Works
The monolith
A monolith is one application. It usually has one codebase, it is deployed as one unit (all parts released together), and it typically uses one database.
Strengths:
- ✅ Simple to build, run and debug. There is one process (one running program), one log stream and one stack trace (the list of function calls that led to an error). The "go to definition" feature of your editor works across all the code.
- ✅ Fast function calls instead of network calls. There is no serialisation (turning data into bytes to send), no network failure and no timeout between modules.
- ✅ Real ACID transactions across features (post 21). A transaction is a group of database changes that either all succeed or all fail. ACID is the set of guarantees that make this safe.
- ✅ Easy refactoring (cleaning up the structure of code), because moving code between modules is a normal code change.
- ✅ Simple deployment and testing: one build pipeline and one output file (artefact) to release.
- ✅ Scales further than people think: run many copies behind a load balancer (a server that spreads requests across copies, post 7).
Weaknesses (mostly as it grows):
- ❌ Team coupling: many teams in one codebase get in each other's way, and releases need planning between teams.
- ❌ Slow builds and deploys as the code grows.
- ❌ Blast radius: this means how much a failure can damage. A memory leak (a bug that slowly uses up memory) in one feature can crash everything (post 40).
- ❌ You scale everything together, even if only search needs more CPU. (To scale means to add more capacity.)
- ❌ One technology stack for everything.
- ❌ Boundaries wear away: without discipline, modules reach into each other's inner parts. The code becomes a "big ball of mud", which means a tangled mess.
Microservices
Microservices split the system into small services that can be deployed on their own. Each service owns one business capability (one job, like payments) and its own data. The services talk to each other over the network (REST, gRPC or events). REST and gRPC are two ways to call another service. An event is a message that says "something happened".
- 1Clientsweb, mobile, partners
- 2API gateway / BFFone front door
- 3User · Order · Payment · Search servicesindependent teams, deploys and scaling
- 4Their own databasesUsers DB · Orders DB · Payments DB · search index — no shared tables
- 5Notification servicereacts to OrderPlaced without the order service knowing
Key principles:
- Organised around business capabilities (orders, payments, catalogue), not around technical layers (like a "database service" or a "UI service").
- Independently deployable: a team ships its service without coordinating with others.
- Database per service: no other service reads its tables directly. They must use its API or events.
- Owned by one team ("you build it, you run it").
- Designed for failure: use timeouts, retries, circuit breakers (stop calling a service that keeps failing) and fallbacks (a backup answer) (Part 7).
Strengths:
- ✅ Team independence: teams deploy on their own schedule, many times a day.
- ✅ Independent scaling: run search on 50 instances (copies) and user profiles on 3.
- ✅ Fault isolation: a failure stays in one place. If recommendations crash, checkout can keep working (with good design, posts 42 and 44).
- ✅ Technology freedom: use the right language or database per service (with limits).
- ✅ Smaller codebases that are easier to understand individually.
Costs (often underestimated):
- ❌ Network calls everywhere: latency, partial failures, timeouts and retries. The "fallacies of distributed computing" (well-known wrong beliefs about networks) apply. The network is not reliable, and latency (the delay of a call) is not zero.
- ❌ Distributed data: there are no transactions across services. So you need sagas (a chain of local steps, with undo steps if one fails), eventual consistency (data becomes the same everywhere after a short delay) and copies of data (posts 23, 28 and 39).
- ❌ Operational overhead: you must deploy, monitor, secure, patch and pay for dozens or hundreds of services. You need strong CI/CD (automatic build and release), containers, orchestration (a system that runs and manages containers), observability and on-call (posts 46–48 and 55–58).
- ❌ Harder debugging: one user request crosses 10 services, so you need distributed tracing (post 46).
- ❌ Harder testing: you need integration tests and contract tests across services. A contract test checks that two services still agree on the shape of their messages.
- ❌ API versioning between services, so that old callers keep working when an API changes (post 34).
- ❌ The "distributed monolith" trap: the services are split, but they are still tightly coupled (they depend closely on each other). They must be deployed together, share a database, or call each other in long chains where each one waits for the next. You get all the costs and none of the benefits.
The middle path: the modular monolith
A modular monolith is one deployable application that is split inside into modules (parts) with strong boundaries:
- +Each module has a public API and private code
- +Each module owns its own tables
- +In-process calls and ACID transactions
- +One deploy, one set of logs
- −Independent deploys and scaling per module
- −Hard fault isolation
- •Boundaries enforced by tooling (e.g. Packwerk), not the network
- Each module has a clear public interface. Others can't reach into its internals, and this is enforced by tooling, package rules or architecture tests.
- Each module owns its tables. Other modules don't query them directly.
- You get most of the simplicity of a monolith and much of the organisation of microservices.
- If a module later needs to become a service (for scaling or team reasons), the boundary is already clean, so extracting it is much easier.
For many companies, a modular monolith is the best long-term architecture, not just a stepping stone.
How to find service (or module) boundaries
- Domain-Driven Design (DDD) is a way of designing software around the business. Split by bounded contexts, which are areas of the business with their own language and rules. "Product" means different things to the catalogue team (description, images), the inventory team (stock levels) and the pricing team (price rules).
- High cohesion, low coupling: cohesion means the parts of a module belong together. Coupling means how much modules depend on each other. Things that change together belong together. If two services always change together, they should probably be one.
- Conway's Law: "organisations design systems that copy their own communication structure." Your architecture will end up matching your team structure, so plan both together. Doing this on purpose is sometimes called the "inverse Conway manoeuvre".
- Data ownership: who is the single source of truth for each piece of data?
Migrating: the strangler fig pattern
Do not rewrite a monolith from scratch in one go. This "big-bang rewrite" fails very often. Instead, replace the pieces step by step. This is like a strangler fig, a vine that slowly grows around a tree until it takes its place:
- Step 1users → monolith
- Step 2add a routing proxy; send /payments to a new Payments service
- Step 3move orders too; the monolith shrinks with every slice
- Eventuallythe monolith is small, or gone — each step was reversible
- Put a proxy or gateway in front of the monolith (post 13). A proxy is a server that receives requests and passes them on.
- Build one new service (say, payments) and route its requests to the new service.
- Move data carefully (post 29). Use dual writes (write to both old and new database), CDC (change data capture, which copies each database change to another system) and checks that both sides match.
- Repeat, one capability at a time, until the monolith is small, or gone.
Signals for each choice
Stay with (or start with) a monolith / modular monolith when:
- the team is small (one or a few teams),
- the business area (domain) is still changing fast and the boundaries are not clear yet,
- you do not yet have strong CI/CD, containers, monitoring and on-call habits,
- speed of product iteration matters more than independent scaling.
Consider microservices when:
- many teams are blocked by each other in one codebase,
- deployments have become slow, risky and heavily coordinated,
- parts of the system have very different scaling, reliability or security needs (like payments vs a marketing blog),
- boundaries are well understood, and
- you have the platform maturity to run many services: automation, observability, service discovery (how services find each other, post 54) and security.
Martin Fowler's well-known advice is "Monolith first." Almost every successful microservices story he saw began with a monolith that grew too big. Systems built as microservices from the start often ran into trouble.
Trade-offs
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Simplicity | ✅ Highest | ✅ High | ❌ Low |
| Team independence | ❌ Low at scale | 🟡 Medium | ✅ High |
| Deploy independence | ❌ | ❌ (one deploy) | ✅ |
| Performance (calls) | ✅ In-process | ✅ In-process | ❌ Network hops |
| Transactions | ✅ ACID | ✅ ACID | ❌ Sagas / eventual |
| Independent scaling | ❌ | ❌ | ✅ |
| Fault isolation | ❌ | 🟡 | ✅ (if designed well) |
| Operational cost | ✅ Low | ✅ Low | ❌ High |
| Best for | Small teams, early products | Most growing products | Large orgs with mature platforms |
In the Real World
Amazon. In the early 2000s, Amazon's large monolith slowed the company down (it became a bottleneck). Amazon moved to services with clear APIs, each owned by a small team. These are the famous "two-pizza teams", small enough to be fed with two pizzas. This change also helped lay the foundation for AWS.
Netflix. In 2008, a major database corruption stopped DVD shipments for several days. After that, Netflix began a move that took years, from a monolith in its own data centre to hundreds of microservices on AWS. It also built many open-source tools (like Eureka, Hystrix and Zuul) to manage the complexity.
Segment's "Goodbye Microservices" (2018). Segment had split its data-delivery system into more than a hundred microservices, one per destination. The result was a lot of operational work, copied code and slow development. It merged them back into a single service, and productivity and reliability improved. It's a famous example of microservices going too far.
Shopify's modular monolith. Shopify runs one of the world's largest Ruby on Rails applications (Rails is a web framework) as a modular monolith. It keeps the boundaries between components with tools, and it even built an open-source tool, Packwerk, to check them. It did not split into many services.
Amazon Prime Video (2023). A Prime Video team described moving its video-quality monitoring tool from a distributed, serverless and microservice design to a single application, cutting infrastructure costs by about 90% for that tool. It started a big discussion in the industry. The key lesson is that the right architecture depends on the workload.
Uber's domain-oriented microservices. After growing to thousands of microservices, Uber described introducing Domain-Oriented Microservice Architecture (DOMA), grouping services into domains with clear gateways, to reduce the complexity of too many tiny, interdependent services.
Interview Questions
Q1When would you choose microservices over a monolith?
When many teams block each other in one codebase, deploys have become slow and heavily coordinated, parts of the system have very different scaling, reliability or security needs, boundaries are well understood, and you have the platform maturity (CI/CD, observability, discovery, on-call) to run many services.
Q2What are the hidden costs of microservices?
Network calls instead of function calls (latency, partial failure, retries), no cross-service ACID transactions (sagas, eventual consistency), distributed debugging and tracing, many deploy pipelines, service discovery and security between services, and duplicated data and contracts to version.
Q3What is a modular monolith?
One deployable application split into modules with explicit public interfaces, private internals and their own tables, with boundaries enforced by tooling. You get most of the organisational benefits without distributed-systems costs, and modules can later be extracted as services.
Q4How do you find good service boundaries?
Follow business capabilities and domain-driven design bounded contexts rather than technical layers; keep things that change together and need transactions together in one service; minimise chatty cross-service calls; align boundaries with team ownership (Conway's law).
Q5How do you migrate from a monolith to services safely?
With the strangler fig: put a routing layer in front, extract one capability at a time behind it, migrate its data with CDC or dual writes, shift traffic gradually with the ability to roll back, and repeat — never a big-bang rewrite.
Q6What does a distributed monolith look like?
Services that must be deployed together, share a database, or call each other synchronously in long chains — all the costs of microservices with none of the independence.
Key Takeaways
- A monolith is simple, fast and transactional, but causes team and deploy coupling at large scale. Microservices give team autonomy, independent deploys and scaling, at a high distributed-systems and operational cost.
- The modular monolith (one deployable, strong internal boundaries, each module owning its data) is often the best choice, and it makes later extraction easy.
- Find boundaries with DDD bounded contexts, high cohesion and low coupling, data ownership and Conway's Law.
- Avoid the distributed monolith (split services that are still tightly coupled or share a database).
- Start with a monolith, and migrate gradually with the strangler fig pattern when team size, deploy pain or scaling needs clearly justify it, and your platform is ready.
Further Reading
- James Lewis and Martin Fowler's article "Microservices", plus Martin Fowler's "MonolithFirst" and "StranglerFigApplication"
- Building Microservices (2nd edition) and Monolith to Microservices by Sam Newman
- Domain-Driven Design by Eric Evans
- microservices.io by Chris Richardson (the microservice architecture pattern language)
- Segment's blog post "Goodbye Microservices: From 100s of problem children to 1 superstar"
- Shopify Engineering's posts on its modular monolith
- The System Design Primer on GitHub (section "Microservices")
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related series