Skip to content
Episode 30 · Twelve services by summer?

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.

30 min intermediateMonolithModular monolithMicroservicesConway's lawThe cost of distributionCoupling & cohesion
  1. Problem

    As a product and its team grow, one codebase becomes a traffic jam: slow deploys, merge conflicts, one bug takes everything down.

  2. 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.

  3. Engineering concept

    Draw boundaries around business domains first (modules), then deploy separately only where independent teams, scaling or failure isolation justify the cost.

  4. Technology

    Monolith, modular monolith with enforced module APIs, microservices with their own data, queues/events between them.

  5. Trade-offs

    Microservices buy team autonomy and independent deploys at the price of operational cost, latency, partial failures and harder consistency.

01

The problem

2 min
Monday, 9:30. Karim walks in with a printed blog post.
  1. Karim · CTO
    Friday: a memory leak in invoice PDFs took all 4 servers down for 12 minutes. About 20 bookings lost.
  2. Karim · CTO
    And finished features wait 3 days for a release slot. Let's split into 12 microservices by summer.
  3. You · Engineer at StayHub
    Would that fix both problems?
  4. Lina · Senior engineer, your mentor
    Guess first.
02

The big idea

4 min
What a split really buys
The big idea

Split 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.

03

How it works

5 min

Think of your options as a ladder. Each step up buys more independence and costs more. Climb only as high as your problem needs.

From one app to many services, cheapest first
  1. 1
    Monolith

    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.

  2. 2
    Modular 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.

  3. 3
    Worker 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.

  4. 4
    Microservice

    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.

  5. 5
    The 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.

  6. 6
    Conway'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.

04

Watch a decision being made

4 min
Worked example

Lina 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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."

    text
    web:    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".

The whole thing in one line: Isolation comes from separate processes, not separate services. Lina fixed the cause for the price of a second start command, and wrote down what would change her answer.
05

In real life

3 min
Two well-known teams, opposite moves

Netflix: 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.

06

What goes wrong

3 min
Calling another service with no timeout

A slow dependency holds your connections until you fall over too.

Instead

A short timeout (say 300 ms), a fallback such as cached prices, and a on every remote call.

Services sharing one database

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.

Instead

Each service owns its data; others use its API or its events.

Big-bang rewrite into services

Months without shipping, two systems to run, boundaries guessed too early.

Instead

: prove the module inside the app, give it its own tables, then move traffic over gradually with a way back.

07

Trade-offs

3 min
OptionFits whenYou payMain risk
MonolithOne team, still learning the productLittle: cheapest to build and runBecomes a tangle; one crash takes all
Modular monolithSeveral teams, roughly 10–100 engineersTooling to keep the walls upTeams still share one release
MicroservicesMany teams, clear domains, a platform teamThe distributed-systems tax, per serviceDistributed monolith
Thursday, 16:00. The architecture discussion.
  1. Karim · CTO
    Modules still share one release train. Search waits on Booking's bugs. Services end that.
  2. Lina · Senior engineer, your mentor
    And add twelve pipelines, on-call and network failures for 14 people. We'd ship less, not more.
  3. Karim · CTO
    We'll be 40 engineers in two years. Splitting later is harder.
08

Your job as tech lead

4 min

Claude 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.

Decisions you own
  • 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.
Tell Claude Code
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.
Check before you accept
  • 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.
Red flags
  • It answers "split into microservices" for one crash, without asking about teams or data ownership.
  • Two "separate" services read and write the same bookings table.
  • A retried payment call has no idempotency key.
09

Check yourself

2 min
Q1/3Reasoning
A booking request must synchronously call 5 services in a chain. Each is available 99.9% of the time, independently. What's the availability of the whole request path, in percent?
%
How confident are you?

Next episode: you and Lina draw the module boundaries inside StayHub, and discover that Search runs 40 queries straight against Booking's tables.

Finished?

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.
10

Technical deep dive

When a function call becomes a network call

Here's what the same booking looks like inside a monolith and across two services. Notice where the new lines come from.

ts
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;
}
Across services: every line marked exists because of the network.
Worked example

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

    text
    web:    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.

  5. 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.

The whole thing in one line: Lina named the pain as isolation, saw that only one extraction force applied, counted the tax, and isolated the leak by running PDFs in worker processes from the same codebase, with a written trigger to extract later.
MonolithModular monolithMicroservices
DeployOne unitOne unitEach service independently
Call between partsFunction call (ns)Function call via module APINetwork call (ms), can fail
DataShared DB, joins anywhereShared server, schema per moduleDatabase per service
TransactionsACID across everythingACID, but module-awareSagas, outbox, eventual consistency
Team scalingPainful past ~20–30 devsGood to ~100 devsHundreds of devs
Ops costLowLowHigh: CI/CD, tracing, on-call per service
Main riskBig ball of mudBoundaries erode without enforcementDistributed monolith
11

Interactive simulation

12

Visual diagram

Click a component

A modular monolith with one extracted service: the pragmatic middle ground many products land on.

13

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.

14

Check your understanding

Q1/4Decision
Company A: 3 developers, 5,000 users, one product (a hotel booking site). Which architecture fits best?
How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a payment timeout with retries

Scenario Warm-up

A 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?

Step 2 · Complete

Finish the extraction plan for Payments

Design Practice

Two 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:

  1. Make Payments a clean module with a public interface inside the monolith.
  2. ___
  3. Build the Payments service behind the same interface.
  4. ___
  5. Move all traffic and delete the old module code.

Fill in steps 2 and 4, and say why each one is in that position.

Step 3 · Solve

Should channel-manager sync become a service?

Decision Practice

The 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.

Step 4 · Decide

Write the answer to Karim

Decision Stretch

Karim 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.

16

Explain it yourself

Your CTO says "we should move to microservices because they scale better." Respond as a senior engineer.

0 words