LearningFoundations of System DesignSystem Design Scope and Architecture

Monoliths and Microservices: Choosing the Shape of a System

Lesson 4 of 4013 minSystem Design Scope and Architecture
In this lesson8 sections

In 2023, Prime Video described consolidating components of a stream-monitoring tool into a monolithic architecture. The example concerned that monitoring workload, not the entire streaming platform. Werner Vogels’ discussion of the redesign contrasts it with a separate distributed Prime Video workflow and emphasizes choosing a structure for the workload.

The post got attention because it ran against the popular story, in which every serious system marches from a monolith toward microservices and never looks back. The real story is less tidy. There are three common shapes for an application. Each is right for some teams at some stage, and moving between them in either direction is normal.

This lesson walks through the three shapes, what each one buys and what it costs, and how to pick one using the requirements you learned to write down in the last lesson.

What you will learn

By the end of this lesson you will be able to:

  • Describe a monolith, a modular monolith, and a microservices architecture, and sketch each one.

  • List what each shape makes easy and what it makes hard.

  • Choose a shape for a given team and product stage, and defend the choice.

  • Spot the traps that turn microservices into a slower, more expensive monolith.

The choice depends on the requirements

There is no best shape. There is only the shape that fits a particular team, at a particular stage, with particular scaling needs. Those are exactly the things the last lesson taught you to write down: how many users, how fast, how reliable, and, just as important, how many engineers you have and how experienced they are at running systems in production.

Keep that list in mind as you read. Each shape below is a good answer to some version of it and a bad answer to others.

The monolith: one application, one deployment

A monolithic application is built and released as one deployment unit. It can contain several modules, run in multiple process instances and use more than one data store. The diagram below shows a simple example with presentation, business and data-access layers using one database.

Internal components of a classic monolithic application
Internal components of a classic monolithic applicationMonolithic applicationUser interfaceBusiness logicData access layerDatabaseOne codebase, one deployment, one running process

The diagram shows the classic layout. A user interface layer on top, business logic in the middle, a data access layer at the bottom, and a single database beneath it all. Every request enters at the top and works its way down inside the same program.

For a small team and a young product, this is often exactly right:

  • Simple to build. One codebase, one set of tools, one place to look for anything.

  • Simple to ship. One thing to deploy. A single push can build and release the whole application.

  • Simple to debug. Everything runs in one process, so you can follow a request from start to finish without it ever crossing a network.

  • Fast to change. A feature that touches the screen, the rules, and the database is one change, not three coordinated ones.

In restaurant terms, a monolith is one kitchen with everyone in the same room. Shouting across the room works fine when there are four of you.

Growth can expose problems with coupling. If modules reach into each other’s internal state or data, a change to order storage may break recommendations. That risk depends on the boundaries and development practices; it is not an inevitable stage on a fixed schedule.

Then scaling becomes all or nothing. If only the search feature is under heavy load, you still have to run more copies of the entire application, because search cannot be separated from the rest.

Then releases become frightening. Every deployment ships all of the code, so a bug anywhere can take down everything, and a single slow team can hold up everyone else's release.

Review question: Does a release require coordinating unrelated changes or accepting an unnecessarily large failure risk? Examine module boundaries, tests and rollout controls before assuming separate services are the only remedy.

The modular monolith: one deployment, clear walls inside

A modular monolith keeps the single program and the single deployment, but organizes the code inside into modules, one per business area. Users, billing, checkout, and so on. Each module has a public interface, and modules talk to each other only through those interfaces. The calls are ordinary function calls inside the same process. No network is involved.

One deployable unit: every module ships together and talks in-process
One deployable unit: every module ships together and talks in-processClientApplication: single deploymentUser moduleBilling moduleCheckout moduleDatabaseModules communicate through in-process calls, not the network

The diagram shows a client talking to one application, and inside that application three modules, each with its own clearly drawn edge, all sharing one database. The important part is the edges. A module's internals belong to that module alone.

One rule makes the whole thing work: a module never reaches into another module's tables or internal code. If checkout needs to know whether a user's payment method is valid, it asks the billing module through billing's interface. It does not run its own query against billing's tables. The moment modules start taking shortcuts around each other's interfaces, the walls are gone and you have an ordinary monolith with extra paperwork.

Note: Document module ownership and check dependency rules in code review or automated checks. Tooling can detect some boundary violations; it cannot decide whether the chosen business boundaries are useful.

What the walls buy you:

  • Code you can find your way around. Everything about billing lives in the billing module.

  • Teams that can own things. One team owns billing, another owns checkout, and they collide far less often.

  • Tests you can run in isolation. A module with a clean edge can be tested on its own.

  • A path to microservices if you need one. A module that already talks to the rest only through an interface can be lifted out into its own service with far less pain.

What they do not buy you: independence at run time. It is still one deployment. A crash in billing can still take down checkout, and you still scale the whole application rather than one piece of it.

Shopify has described componentizing its monolith and using Packwerk to check dependency boundaries. Its published account also discusses the difficulty of establishing strong boundaries in an existing codebase. This is an example of modularization work, not evidence that every module can be isolated immediately. Shopify’s account explains the tooling.

In the kitchen, this is the moment someone tapes lines on the floor and labels the stations. Same room, same building, but the grill cook no longer wanders into pastry.

Microservices: many small applications

In a microservices architecture, the product is split into separate services, each owning one business area, each running as its own program, and usually each with its own database. The services talk to each other over the network through APIs.

Microservices behind an API gateway, each with its own database
Microservices behind an API gateway, each with its own databaseClientAPI gatewayService ADatabase AService BDatabase BService CDatabase CEach service owns its own database; none share tables

The diagram shows an API gateway routing client requests to three services, each with its own database. This is one possible deployment. The design goal is explicit ownership and controlled dependencies; services may still share infrastructure and common operational tools.

The restaurant is now a food court. Each stall is its own business with its own kitchen and its own till. The food court provides the entrance and the seating, and that is all.

What the split buys you:

  • Independent releases. The billing team can ship on Tuesday without asking the search team. Nothing else is rebuilt.

  • Independent scaling. If search is the hot spot, run more copies of search and nothing else.

  • Failures that stay contained. If recommendations go down, checkout can keep working, provided it was designed to cope with a missing neighbor.

  • Freedom of tools. Each service can use the language and database that suit its job, within reason.

And what it costs, which is the part the popular story leaves out:

  • Network boundaries add failure modes. A remote call can be delayed, lost, or completed without a response reaching the caller. Section 3 develops the network model, and section 5 covers deadlines, safe retries, and recovery.

  • The data is split. Each service owns its own data, so a question that used to be one database query is now several network calls, and keeping the pieces in agreement is hard. The word for the compromise most teams accept is eventual consistency: the copies agree, but not instantly. Sections 2 and 6 go deep on it.

  • Operations span services. Independent deployments need observability, discovery, and clear ownership. Section 2 introduces runtime resources and telemetry. A dedicated platform team is one organizational option, not a prerequisite for every service architecture.

Hypothetical example: A five-person team considering separate services should estimate the work of deployment, monitoring and failure handling alongside feature development. Independent scaling or isolation may justify that work. Team size alone does not decide the architecture.

The three shapes side by side

MonolithModular monolithMicroservices
How many programsOneOneMany
How many deploymentsOneOneOne per service
How the parts talkDirect calls, anywhereCalls through module interfacesNetwork calls through APIs
DatabasesOne, sharedOne, sharedUsually one per service
ScalingWhole applicationWhole applicationEach service on its own
A failure in one partCan take down everythingCan take down everythingCan stay contained, if designed for it
Team fitOne small teamA few teams owning modulesMany teams owning services
Operational effortLowLow to mediumHigh

How to choose

Four questions decide most cases.

Who owns the components? Compare service boundaries with communication and decision-making needs. Separate teams may benefit from independent releases, while tightly coordinated work may be easier within one deployment. Conway’s law is a useful observation about this relationship, not a deterministic formula that converts a head count into an architecture.

What stage is the business at? A product still searching for its market needs to change direction weekly. That calls for one program that one small team can reshape fast. A mature product with known hot spots and stable boundaries can afford to split those hot spots out.

Do the parts need different scaling? If everything grows together, one program scales fine. If one feature carries a hundred times the load of the others, the ability to scale it alone starts paying for the extra complexity.

Can you run it? Be honest about this one. Do you have automated deployments, monitoring, on-call rotations, and people who have operated a distributed system before? If not, the operational cost of microservices will swallow the benefits.

For a small team exploring a product with uncertain boundaries, a modular monolith is a reasonable starting point. Revisit the choice when a component needs independent scaling, release timing or failure isolation. Extraction still requires work on data ownership, interfaces, observability and migration; an existing module boundary reduces some of that work but does not eliminate it.

The Prime Video monitoring example illustrates why a design should be revisited when workload evidence changes. Evaluate where time, cost and operational complexity accumulate, then compare alternatives against those observations.

Common traps

  • Copying a big company's architecture without its problems. Their shape solves their team size and their scale. You have neither yet.

  • Splitting by technical layer instead of business area. A "database service," a "logic service," and a "screen service" means every feature touches every service. Split by what the business does: orders, billing, search.

  • Uncontrolled shared data ownership. Services that freely modify each other’s tables can require coordinated changes. Sharing a database server is different from sharing schema ownership; make those boundaries explicit before assessing independence.

  • Splitting too fine. Dozens of tiny services that must all change together are a monolith with extra network hops.

  • Assuming more pieces means more reliability. Without timeouts, retries, and fallbacks, a network of services fails more often than one program, because there are more things to fail.

Interview tip: State a default and justify it from the team and the stage. "I'd start with a modular monolith, because it's a small team and we don't know the hot spots yet. I'd pull search out into its own service once it needs to scale separately." One sentence like that shows you understand both the benefits and the costs, which is what the interviewer is checking.

Knowledge check

Four questions. Reason from team size, stage, and operational maturity before you answer.

Knowledge check

Check your understanding

4 questions · source answers hidden

Question 1 of 4

A five-person engineering team has modest traffic and limited production operations experience. It needs clear module boundaries but has no demonstrated need for independent service deployment. Which starting design best fits these constraints?

A.

Microservices, so the system is ready for the growth the founders expect.

B.

A modular monolith: one program and one deployment, with enforced walls between business areas so pieces can be split out later if they earn it.

C.

A classic monolith with no internal structure, since structure can be added later.

D.

A separate service for every screen in the app.

Question 2 of 4

A previously in-process function call becomes a call to a separate network service. Which additional failure mode must now be handled?

A.

The codebase becomes harder to understand as it grows.

B.

A call between two parts of the product can now fail because the network between them failed.

C.

Every release ships all of the code at once.

D.

Only one team can work on the code at a time.

Question 3 of 4

A company has twelve services that all read and write the same database and must be released together. What have they built?

A.

A well-designed microservices system.

B.

A modular monolith.

C.

A distributed monolith: separate processes with tightly coupled data and releases.

D.

A monolith, since there is only one database.

Question 4 of 4

When is it a good moment to pull a module out of a modular monolith into its own service?

A.

As soon as the module grows past a certain number of lines of code.

B.

When the module needs to scale, release, or be owned independently of the rest.

C.

Never. A modular monolith should stay a single deployment forever.

D.

Whenever a new engineer joins the team.

Key takeaways

  • There are three common shapes: a monolith (one program), a modular monolith (one program with enforced walls between business areas), and microservices (many programs talking over a network).

  • A monolith simplifies deployment, while coupling and whole-application scaling can become constraints as requirements change.

  • A modular monolith keeps the simplicity and adds boundaries. The one rule is that modules talk only through their interfaces.

  • Microservices buy independence at the price of network failures, split data, and heavy operations. Adopt them when you have the problems they solve.

  • Choose from team size, business stage, scaling needs, and operational maturity. Default to a modular monolith and extract services when a module earns it. Moving in either direction is normal.

The next section develops the runtime model beneath these application boundaries. Start with processes and threads: what state they share, how they use CPU and memory, and what can fail independently. Networking and distributed coordination then build on that foundation.