Monolith vs microservices
Monday, 9:30: a memory leak in invoice PDFs took all four StayHub servers down for 12 minutes on Friday, and features wait three days for a release slot. Karim wants twelve microservices by summer.
One big app or many small ones? Neither is better. Each has a price, and the right one depends mostly on your team.
- Problem
As a product and its team grow, one codebase becomes a traffic jam: slow deploys, merge conflicts, one bug takes everything down.
- Why the simple system fails
Splitting too early trades a code problem for a network problem: every call can now fail, be slow, or be out of date, and every feature needs coordinated releases.
- Engineering concept
Draw boundaries around business domains first (modules), then deploy separately only where independent teams, scaling or failure isolation justify the cost.
- Technology
Monolith, modular monolith with enforced module APIs, microservices with their own data, queues/events between them.
- Trade-offs
Microservices buy team autonomy and independent deploys at the price of operational cost, latency, partial failures and harder consistency.
The problem
2 min- Karim · CTOFriday: a memory leak in invoice PDFs took all 4 servers down for 12 minutes. About 20 bookings lost.
- Karim · CTOAnd finished features wait 3 days for a release slot. Let's split into 12 microservices by summer.
- You · Engineer at StayHubWould that fix both problems?
- Lina · Senior engineer, your mentorGuess first.
The big idea
4 minSplit a system for team independence or isolation, never for fashion: every split adds a network tax to every call.
So Karim has two different problems. The 3-day wait is a team problem: three teams share one release train. The crash is an isolation problem: PDF code runs inside the processes that serve guests. Neither one needs twelve services.
How it works
5 minThink of your options as a ladder. Each step up buys more independence and costs more. Climb only as high as your problem needs.
- 1Monolith
A is one app, deployed as one unit, usually with one database. Parts talk by function calls: instant, and they can't half-fail. A booking and its payment are saved together in one transaction.
- 2Modular monolith
A is still one deploy, with walls inside. Each module (Search, Booking, Payments) has a public interface and owns its tables. A lint rule blocks reaching into another module.
- 3Worker processes
The same code, started in a second worker mode that runs heavy jobs (PDFs, emails) from a queue. A crash kills a worker, not the web servers. Isolation without a network.
- 4Microservice
A separate app with its own deploy and its own data. Other parts reach it only over the network, through its API or its events. Its team ships whenever it likes.
- 5The distributed-systems tax
What every split costs, forever: timeouts, retries that must not charge a card twice, no transaction across services, and a pipeline, dashboards and on-call for each service.
- 6Conway's law
: systems end up shaped like the teams that build them. So draw service lines along teams and business areas (Bookings, Payments), never along technical layers.
Watch a decision being made
4 minLina decides what to do about the invoice leak
A memory leak in invoice PDFs crashed all 4 servers for 12 minutes. Karim wants an Invoicing microservice. Find the smallest change that stops it happening again.
- 1Name the pain precisely
PDFs run in the same processes that serve guests, so a leak there kills search and booking too. The pain is failure spreading, not teams waiting.
Why: A people problem and an isolation problem have different fixes.
- 2Run the extraction checklist
Is it owned by its own team, blocked by the release train? No, Booking owns it. A different workload? Yes, memory-heavy. Needs isolation? Yes. A stable boundary? No, it changes with every pricing feature.
Why: A service earns its tax only when several of these push at once. Here both yeses are the same need, isolation, and that usually has a cheaper answer.
- 3Count the tax
A service means a new pipeline, dashboards, on-call, a versioned contract with Booking, and a plan for "invoice requested, service down". For 14 people: weeks of work, then a cost forever.
Why: If you don't count the bill before splitting, you discover it in incidents.
- 4Take the smallest move, and write a trigger
Run PDFs in worker mode: same repo, same release, separate processes. Then write down: "Extract Invoicing when a dedicated team owns it."
textweb: node dist/main.js --mode=web (4 servers, behind the load balancer) worker: node dist/main.js --mode=worker (2 processes, PDF + email jobs)Why: It isolates the leak with no network, and turns "never" into "not yet".
In real life
3 minNetflix: many services
- Publicly described moving from one large app to many cloud services after a major database outage
- Hundreds of engineers; each team owns and deploys its own services
- Invested heavily in tooling for failures between services
Worth it with many teams and a platform to carry the tax.
Prime Video monitoring: back to one
- A team publicly described merging a distributed video-monitoring tool into a single process
- Its parts constantly passed data to each other over the network
- They reported a large drop in infrastructure cost
Splitting hurts when the parts always work together.
What goes wrong
3 minA slow dependency holds your connections until you fall over too.
A short timeout (say 300 ms), a fallback such as cached prices, and a on every remote call.
One team's schema change breaks another's, so deploys go in lock-step. That's a : all the network costs, none of the independence.
Each service owns its data; others use its API or its events.
Months without shipping, two systems to run, boundaries guessed too early.
: prove the module inside the app, give it its own tables, then move traffic over gradually with a way back.
Trade-offs
3 min| Option | Fits when | You pay | Main risk |
|---|---|---|---|
| Monolith | One team, still learning the product | Little: cheapest to build and run | Becomes a tangle; one crash takes all |
| Modular monolith | Several teams, roughly 10–100 engineers | Tooling to keep the walls up | Teams still share one release |
| Microservices | Many teams, clear domains, a platform team | The distributed-systems tax, per service | Distributed monolith |
- Karim · CTOModules still share one release train. Search waits on Booking's bugs. Services end that.
- Lina · Senior engineer, your mentorAnd add twelve pipelines, on-call and network failures for 14 people. We'd ship less, not more.
- Karim · CTOWe'll be 40 engineers in two years. Splitting later is harder.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- Whether a part becomes a service at all. Run the checklist yourself (own team, different workload, needs isolation, stable boundary) before Claude scaffolds a new repo.
- Who owns which data. A new service gets its own tables; no other code reads them directly.
- How every new network call fails: its timeout, retries with an , and what the guest sees when the other side is down.
Invoice PDFs leaked memory and crashed all 4 StayHub app servers for 12 minutes. Don't create a new service. Plan first, don't edit yet: run invoice PDF and email jobs in a separate worker mode of the same monolith (same repo, same release), reading from our existing queue. Show me which files change, how the worker starts, how a retried job avoids sending two invoices, and how worker crashes show on our dashboards. Don't touch booking or payment code.
- The web processes behind the load balancer never import the PDF code.
- No new repo, pipeline or network contract. If there is one, Claude built a service you didn't ask for.
- Every remaining service call (like pricing) has a timeout value Claude can point to.
- It answers "split into microservices" for one crash, without asking about teams or data ownership.
- Two "separate" services read and write the same
bookingstable. - A retried payment call has no idempotency key.
Check yourself
2 minNext episode: you and Lina draw the module boundaries inside StayHub, and discover that Search runs 40 queries straight against Booking's tables.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
Here's what the same booking looks like inside a monolith and across two services. Notice where the new lines come from.
const booking = await bookings.create(cmd); // committed in Bookings DB
try {
await paymentsClient.capture(
{ bookingId: booking.id, amountCents: booking.totalCents },
{ timeoutMs: 2000, retries: 3, idempotencyKey: booking.id }, // retries need idempotency
);
} catch (err) {
// Did Payments charge the card before timing out? We don't know.
await bookings.markPaymentPending(booking.id); // reconcile later
throw err;
}Lina decides what to do about the invoice leak
A memory leak in PDF invoice generation crashed all 4 servers for 12 minutes. Karim proposes extracting an Invoicing microservice. Decide the smallest change that stops this from happening again.
- 1Name the pain precisely
Invoice PDFs run inside the same processes that serve guests. A leak there kills search, login and booking too. The pain is failure spreading, not team waiting.
Why: The fix depends on the kind of pain. A people problem and an isolation problem have different answers.
- 2Run the extraction checklist
Does Invoicing have its own team? No, Booking owns it. A different workload? Yes, CPU and memory heavy. Needs isolation? Yes. Is the boundary stable? It changes with every pricing feature. Two yeses out of four, and both are really the same need: isolation.
Why: A service is worth its tax only when several independent forces push at once. When every yes points at one need, there's usually a cheaper answer.
- 3Count the tax
A separate service needs its own pipeline, dashboards, on-call, a versioned contract with Booking, and a way to handle "invoice requested, service down". For a 14-person team, that's weeks of work and a permanent cost.
Why: If you don't count the bill before splitting, you discover it in incidents.
- 4Find the smallest move that removes the cause
Run PDF generation in worker mode: the same codebase, started as separate processes that read jobs from a queue. A leak now kills a worker, which restarts, and guests never notice.
textweb: node dist/main.js --mode=web (4 servers, behind the load balancer) worker: node dist/main.js --mode=worker (2 processes, PDF + email jobs)Why: It gives the isolation we need with no network contract, no new repo and no second release train.
- 5Set a revisit trigger
Write it down: "Extract Invoicing as a service if a dedicated team owns it, or if worker deploys start blocking web deploys."
Why: It turns "never" into "not yet", and tells Karim exactly what would change the answer.
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Deploy | One unit | One unit | Each service independently |
| Call between parts | Function call (ns) | Function call via module API | Network call (ms), can fail |
| Data | Shared DB, joins anywhere | Shared server, schema per module | Database per service |
| Transactions | ACID across everything | ACID, but module-aware | Sagas, outbox, eventual consistency |
| Team scaling | Painful past ~20–30 devs | Good to ~100 devs | Hundreds of devs |
| Ops cost | Low | Low | High: CI/CD, tracing, on-call per service |
| Main risk | Big ball of mud | Boundaries erode without enforcement | Distributed monolith |
Interactive simulation
Visual diagram
A modular monolith with one extracted service: the pragmatic middle ground many products land on.
Analogy
- Places to eatOne kitchen, one team, shouting acrossWays to build softwareA monolith
- Places to eatOne kitchen with separate stationsWays to build softwareA modular monolith
- Places to eatA food court of separate stallsWays to build softwareMicroservices
- Places to eatPaying for a meal from three stallsWays to build softwareA request spanning several services
- Places to eatDrinks stall closes, combo meals breakWays to build softwareOne service down breaks a flow
Where the analogy breaks: food-court stalls rarely need each other. Real services often do, so every extra split adds another call that can be slow or fail.
Check your understanding
Practice ladder
Trace a payment timeout with retries
Scenario Warm-upA team at another company split Booking and Payments into two services. Booking calls Payments like this:
await paymentsClient.capture({ bookingId, amountCents: 42000 }, { timeoutMs: 2000, retries: 3 });There is no idempotency key. On Saturday, Payments is slow: it charges the card after 2.5 s each time, then answers.
Trace one booking: how many calls does Booking make, how many charges hit the guest's card, and what does Booking believe happened?
Finish the extraction plan for Payments
Design PracticeTwo years from now StayHub has 40 engineers and a dedicated Payments team that's blocked by the shared release train. Lina's plan to extract Payments has two missing steps:
- Make Payments a clean module with a public interface inside the monolith.
- ___
- Build the Payments service behind the same interface.
- ___
- Move all traffic and delete the old module code.
Fill in steps 2 and 4, and say why each one is in that position.
Should channel-manager sync become a service?
Decision PracticeThe Hotel Partners team (4 engineers) runs channel-manager sync inside the monolith: every 5 minutes it pulls rates and availability from 1,800 hotels' channel managers. Facts:
- It's bursty: CPU spikes to 80% on all 4 web servers for ~40 s every 5 minutes, and guest search slows down during spikes.
- Partner APIs are flaky; the sync code changes weekly as new channel managers are added.
- The rest of the monolith rarely changes the sync code.
- Hotel Partners already owns this code alone.
Decide: extract it as a service, run it in worker mode, or leave it. Use the extraction checklist and say what you'd watch to change your mind.
Write the answer to Karim
Decision StretchKarim asks for a written decision on his proposal: 12 microservices by summer.
Facts: 14 engineers, 3 teams, one on-call rotation, no platform team. Pains: 3-day wait for release slots; one memory leak crashed all servers. Most features touch booking, pricing and payments together. Traffic: ~30,000 guests/day on 4 servers at 35% CPU.
Write a short decision: "We chose X over Y because…, accepting the risk of…", and give one trigger that would make you revisit it.
Explain it yourself
Your CTO says "we should move to microservices because they scale better." Respond as a senior engineer.