0 XP · 0 scenes

Client-server and the monolith

Architecture Patterns · lesson 1 of 7 · about 26 min

Client-server and the monolith

The simplest shape and why to start there: a single deployable behind a client-server interface, what team growth does to a shared codebase and how modules help, scaling by cloning until the database is the limit, why big release trains are risky, how test time grows, when splitting genuinely pays, and why the big-bang rewrite almost never catches up. Tap any for a plain-language card.

0 / 9 sections done
  1. 1The simplest shape5 min
  2. 2Scaling a monolith4 min
  3. 3Deploying the whole thing4 min
  4. 4Feedback time3 min
  5. 5When to split4 min
  6. 6Which move?2 min
  7. 7Who lives by this1 min
  8. 8Interview angle2 min
  9. 9Break It: the big-bang rewrite3 min

Words in this lesson8Tap one for its meaning.

The simplest shape

One program, one database, one deploy. Why most successful systems started here.

The pressure appears as the team grows. Brooks observed that coordination paths grow with the square of the headcount: n people have n(n−1)/2 pairs who may need to talk. In one codebase, more developers touching the same hot files means more merge conflicts and more stepping on each other. A modular monolith keeps one deployable but enforces boundaries inside (modules with clear interfaces and owners), which removes much of that friction without a network between the parts.

Grow the team from 8 to 40 in a single module: conflicts per week jump. Then add modules.

  • 2 developers
    0.0 conflicting pairs a week
  • 5 developers
    0.3 conflicting pairs a week
  • 10 developers
    1.5 conflicting pairs a week
  • 25 developers
    10.2 conflicting pairs a week
  • 50 developers
    40.9 conflicting pairs a week
Pairs of people who may need to coordinate28
Overlapping edits per week0.9
Weeks with at least one overlap60%

8 developers have 28 possible conversations. Each edits 5 files a week in 2,000 files across 1 module (30% of edits land on hot files): that is 0.9 overlapping edits a week, and 60% of weeks have at least one.

Seeded 400-week simulation: 5 file edits per developer per week, 30% of them on a hot 5% of files; developers are spread evenly over modules and edit within their own module.

Scaling the single program

Add servers and throughput grows. Then it stops growing, and the server count isn't the reason.

The shared thing is usually the database. Ten app servers all talk to one database, so once the combined traffic they send exceeds what it can serve, adding the eleventh server adds nothing. The remedies are the earlier lessons: a cache that absorbs reads, read replicas, indexes, and finally sharding. Below, the app tier is 500 requests a second per server, the database serves 3,000 queries a second, and 80% of requests are reads.

With no cache, throughput flattens at 3,000 from about six servers. Put 80% of reads in a cache and the same database supports 8,000.

  • 1 server
    500 req/s · limited by the app servers
  • 2 servers
    1,000 req/s · limited by the app servers
  • 5 servers
    2,500 req/s · limited by the app servers
  • 10 servers
    3,000 req/s · limited by the database
  • 20 servers
    3,000 req/s · limited by the database
  • 40 servers
    3,000 req/s · limited by the database
App tier could handle5,000 req/s
Database lets through3,000 req/s
System throughput3,000 req/s

10 instances can serve 5,000 requests a second, but the shared database only lets 3,000 through (100% of requests reach it). Throughput is 3,000, limited by the database. More servers will not help: cache, replicate or shard.

Database load per request = 1 − 0.8 × cache hit share (80% of requests are reads). Throughput = min(instances × per-instance, database capacity ÷ database share).

Deploying the whole thing

Forty changes on the release train. One of them is broken. Which one holds up the other thirty-nine?

The remedy is not microservices first; it is smaller, more frequent releases from the same monolith (trunk-based development, feature flags, canary deploys from the deployments lesson). Releasing every day with a handful of changes makes each release low-risk and easy to roll back. The same code, shipped continuously instead of on a fortnightly train, has far less drama.

At 40 changes every 14 days, 70% of trains are blocked. At daily releases with about three changes each, it is under 10% and a block delays only a day.

  • release every 1 day
    9% of releases contain a bad change · wait ≈ 0.6 days
  • release every 7 days
    46% of releases contain a bad change · wait ≈ 6.7 days
  • release every 14 days
    70% of releases contain a bad change · wait ≈ 16.9 days
  • release every 30 days
    93% of releases contain a bad change · wait ≈ 42.8 days
Releases that contain a bad change70%
Average wait for a finished change16.9 days
Releases a year26

40 changes per release with a 3% defect rate each means 70% of releases contain at least one bad change. With a train every 14 days, a finished change waits 16.9 days on average. Shipping more often shrinks both.

P(bad release) = 1 − (1 − q)^changes. Delay = half the interval waiting for the train, plus a full interval whenever the train is blocked. The comparison rows keep the same overall change rate.

Feedback time

Every change runs every test. After three years, “every test” takes an hour.

There is a middle path before splitting the deployment: make the build aware of structure. With clear module boundaries and a build tool that knows the dependency graph (Bazel, Nx, Gradle), a change to one module re-tests only the modules that depend on it. Parallelising tests across machines (sharding) helps too, at the price of more machines. Set the size of your suite.

200,000 tests at 100 ms each take about 22 minutes even on 20 machines, set-up included. Test only the affected 10% and it drops to under 7.

  • Run every test
    21.7 min
  • Run only affected tests
    6.7 min
Feedback time, all tests21.7 min
Feedback time, affected only6.7 min

200,000 tests of 100 ms spread over 20 machines give 21.7 min of feedback (including 5 minutes of fixed set-up). Running only the 10% that depend on the change takes 6.7 min. Past about 15 minutes people stop waiting and start batching.

Time = tests × duration ÷ machines + 5 minutes of fixed set-up (checkout, build, start-up).

When to split

“We should move to microservices” is a conclusion. What problem is it meant to solve?

A practical rule is Conway's law: systems end up mirroring the communication structure of the organisation that builds them, so split along team boundaries, not technical layers, and only when the coordination cost of the shared codebase is hurting. Score your situation below; the result is a heuristic, not a verdict, but it shows which forces actually point toward splitting.

A team of 15 with no need for independent deploys scores low: stay modular. Raise the team to 60 and the need for independent releases and the score crosses into “split”.

Score toward splitting3
Suggestionstay a (modular) monolith

15 engineers, independence somewhat, scaling skew somewhat, technology variety no, operations decent: score 3. Suggestion: stay a (modular) monolith. Weak operations push the score down on purpose, because many services need more tooling, not less.

A simple additive heuristic: team size (0 to 3 points) + the three needs (0 to 3 each) + twice the operations maturity − 2. It illustrates which factors matter, not a calibrated model.

Which move?

Six situations, four moves.

Stay simple until a specific pain justifies a more complex shape, and migrate in steps that each ship. Each answer is worth 20 XP.

Ask: what specific problem am I solving, and what is the smallest change that solves it?

Question 1 / 6

A team of six is building an MVP and wants to move fast.

Tip: press 1 to 4 to answer, Enter for the next one.

Who lives by this

From the majestic monolith to Prime Video.

Four published views on monoliths and splitting.

A defence of the monolithDHH (Basecamp), 2016

'The Majestic Monolith'

David Heinemeier Hansson argued that a single, well-organised application serves small and medium teams better than a fleet of services, and that the costs of distribution are paid up front for benefits most teams never need.

Modular at scaleShopify

a large modular monolith

Shopify has written about keeping its large Ruby on Rails application as a modular monolith, with enforced component boundaries and ownership, rather than splitting it into hundreds of services, to keep development fast while the team grew.

Back to one processAmazon Prime Video (2023)

video-quality monitoring

Prime Video's engineering team described moving a video-quality monitoring tool from a distributed set of serverless components back to a single process, reporting a cost reduction of about 90% because the data passing between components was the expense.

The safe migrationMartin Fowler

the Strangler Fig application

Fowler described incrementally replacing a legacy system by growing the new one around it, routing more and more functionality to the new code until the old can be removed, named after the vine that gradually envelops a tree.

Interview angle

What interviewers actually ask.

Six questions, each worth 20 XP. 'Would you use microservices?' is a standard architecture prompt; the strongest answers start with the monolith.

Say what concrete problem splitting solves, and what it costs.

Question 1 / 6

Why is a monolith a good starting point?

Tip: press 1 to 3 to answer, Enter for the next one.

Break it

“Let's rewrite it properly, in six months.” Two years later the old system still earns all the money.

The proven alternative is the pattern: build new functionality (or re-implement one slice at a time) as new modules or services behind a routing layer, send traffic to them piece by piece, and let the old code shrink until it can be deleted. Every step ships to production and earns its keep; if the effort stops, you still have something better than you started with. Set the sizes.

With 500 features, the old system adding 20 a month and the rewrite shipping 25, parity takes 100 months. Raise the rewrite rate to 60 and it takes a year and a quarter.

13420now1 yr2 yr3 yr
Gap the rewrite closes each month5 features
Time to reach parity100 months (8.3 years)

The old system has 500 features and grows by 20 a month; the rewrite ships 25. It closes the gap by 5 a month, so parity takes 100 months (8.3 years) and the business sees no benefit until then. The strangler approach delivers value at every step instead.

Parity time = features ÷ (rewrite rate − old growth rate). A deliberately simple model that ignores learning, bugs and the features nobody remembers.

Where to go next