A monolith builds, tests and deploys your whole application as one unit. Microservices split it into services that each deploy, scale and fail on their own, and each own their data. Between them sits the modular monolith: one deployable with boundaries the build enforces. The choice depends less on technology than on how many teams change the system at once, and whether you can pay the network, data and operations costs every split adds.
Picture it first: one kitchen or a food court
A busy restaurant runs one kitchen. Every station shares one fridge and one opening time, and the cook who needs paneer just walks over and takes it. Fast and simple, until there are 60 cooks, nobody knows whose shelf is whose, and one spilt pan of oil closes everything.
A well-run big kitchen fixes this without moving out: each station owns its shelves, and anyone who needs something from another station asks at the pass.
A food court goes further. Every stall has its own kitchen, fridge, staff and hours, and can renovate on Tuesday without asking anyone. But a thali that needs dal from one stall and rotis from another now needs a runner, and if the roti stall is shut, the thali arrives incomplete.
The worst version is a food court whose stalls share one fridge and must all close for renovation on the same day: you pay for the runners and get none of the independence.
Map it back: the single kitchen is a monolith, the kitchen with owned stations is a modular monolith, the food court is microservices, and the food court with a shared fridge is a distributed monolith. The runner is the network; the fridge is the database.
Four shapes, precisely
Monolith. The application is built, tested and deployed as one unit, usually behind a load balancer with one database. The word describes how you deploy, not how good the code is: a monolith can be cleanly layered or a big ball of mud. You scale it by running identical copies.
Modular monolith. Still one deployable, but divided into modules along business lines (orders, payments, catalogue). Each module exposes a public interface and keeps its tables private, and a CI check fails the build when one module reaches into another’s internals. Calls stay in-process, yet the seams are real.
Microservices. James Lewis and Martin Fowler’s 2014 article describes one application built as a suite of small services, each in its own process, talking over lightweight mechanisms such as HTTP or simple messaging. Of their nine characteristics, five carry most of the weight:
- Componentization via services. Each service is independently deployable, so most changes ship without redeploying anything else.
- Organised around business capabilities. “Checkout” or “search”, not “the UI layer”; one cross-functional team owns each.
- Decentralised data. Each service owns its data and schema; everyone else goes through its API.
- Smart endpoints and dumb pipes. Logic lives in services; the transport just moves messages, unlike an enterprise service bus that routes and transforms.
- Decentralised governance. Teams pick their own tools and share proven libraries instead of following central mandates.
Notice what the list leaves out: size. “Micro” is about independence, not line count.
Distributed monolith. Many deployables that behave like one: they share a database, release in lockstep, or cannot serve a request unless all of them are up. You get the network costs of microservices and the coupling of a monolith. Uber’s engineers call these “networked monoliths”.
In the interview
Say it out loud: “Deployment units and module boundaries are separate axes.” The safe path adds boundaries first, then splits along them. Splitting a tangled codebase into services skips the first step and lands in the distributed monolith.
What splitting buys you
Independent deploys. A team ships many times a day without a release train, and a bad change rolls back without touching anyone else. In a large monolith every deploy carries everyone’s changes. Uber names risky, expensive deployments with frequent rollbacks as a main reason it broke up its two monoliths around 2012–2013.
Team autonomy. One team, one service, one backlog, one on-call rotation. Ownership is obvious, and a new engineer learns one service, not the whole system.
Per-service scaling. During a big sale, search might take 10× its usual traffic while seller tools stay flat. You add search pods only, on hardware that suits search.
Fault isolation. A memory leak in recommendations kills recommendations, not payments, if every caller uses timeouts and fallbacks. Without them, one slow service ties up its callers’ threads and the failure cascades as it would in a monolith.
Technology freedom. A team can pick a different language or database; use it sparingly, since every extra stack needs its own expertise.
“Scale independently” is often oversold, so check the arithmetic.
Here a monolith copy needs 4 vCPU and 8 GB, four copies run normally (16 vCPU), and search is 30% of the CPU work. When search goes 10×, load is 0.7 + 0.3 × 10 = 3.7×, so you need 16 × 3.7 ≈ 60 vCPU: 15 copies and 120 GB. Split out on 2 vCPU, 2 GB pods, search needs 4.8 × 10 = 48 vCPU (24 pods) and the rest fits in 6 pods: again about 60 vCPU, but only 60 GB. The CPU bill barely moves; memory, start-up time and the blast radius of scaling do. Sometimes pooling wins instead: Segment found one large pool of mixed workers absorbed spikes better than 140 small services, each with autoscaling to tune.
The price: latency and availability
Every boundary you turn into a network call pays a toll. An in-process call costs nanoseconds. A call to another service in the same data centre adds serialisation, a network round trip, often a sidecar or load balancer, and deserialisation. As an illustrative order of magnitude, budget 0.5–1 ms per hop at the median and several times that at p99 (see numbers to know, and measure your own).
Ten hops add about 10 ms, which is tolerable. The danger is the naive conversion Lewis and Fowler warn about: a page that made 200 in-memory module calls now makes 200 network calls, roughly 200 ms if they run one after another. That is the chatty I/O anti-pattern; the cures are coarser APIs, batch endpoints, and keeping data that is read together in one service.
Availability multiplies. If a request needs N services in series, each up 99.9% of the time and failing independently, it succeeds with probability 0.999^N. For ten services, 0.999^10 ≈ 0.990: 99.0%. Over a 30-day month (43,200 minutes), 99.9% allows 43 minutes of failure; 99.0% allows about 430 minutes, or 7.2 hours. Lewis and Fowler call this the multiplicative effect of downtime, and cite The Guardian’s rule of one synchronous call per user request. See availability and the nines for the full arithmetic.
Tail latency compounds too. If a page fans out to 10 services in parallel and each is slower than its own p99 1% of the time, at least one is slow with probability 1 − 0.99^10 ≈ 9.6%. Each service’s p99 becomes roughly the page’s p90.
What you do about it:
- Keep synchronous chains short: two or three hops per user request, with the rest moved to events.
- Bound every wait with timeouts and retries with jitter, a circuit breaker per dependency, and bulkheads so one slow dependency can’t take every thread.
- Degrade, don’t fail. If recommendations are down, show bestsellers. Checkout must still work.
The price: data and transactions
In a monolith, placing an order is one database transaction: insert the order, decrement stock, record the payment, commit. Any failure rolls back all three. Once orders, inventory and payments are separate services with separate databases, that transaction is gone. You cannot join across services, and two-phase commit across them is rarely worth the blocking and coupling (see 2PC vs sagas). The usual replacement is a saga: local transactions, one per service, linked by events, with a compensating action for each step that may need undoing.
Three details make sagas work:
- Publish events reliably. Saving the order and publishing “OrderPlaced” touch two systems; crash in between and you lose the event or announce an order that was never saved. The transactional outbox writes the event in the same local transaction and relays it afterwards.
- Make consumers idempotent. Events arrive at least once, so “charge ₹499 for order 7781” must be safe to process twice (idempotency).
- Design for in-between states. For a few seconds a customer can see a pending order with a captured payment; screens and support tools must say so.
Reads change too: a “my orders” page that joined three tables now calls three APIs (API composition) or reads a lagging view built from events.
The price: a platform before features
Fowler calls this the microservice premium: automated deployment, monitoring, failure handling and eventual consistency slow a team down until the system is complex enough to need them. His guideline is blunt: don’t even consider microservices unless the system is too complex to manage as a monolith. His prerequisites are just as short:
- Rapid provisioning: a new server or environment in hours, not weeks.
- Basic monitoring: errors, availability and business signals such as a drop in orders.
- Rapid application deployment: a pipeline that reaches production in no more than a couple of hours, with quick rollback.
- A DevOps culture: developers and operators handle incidents together.
Past a handful of services, he adds tracing a business transaction across services and fully automated delivery. In practice the platform looks like this:
| Capability | Why services need it | Read more |
|---|---|---|
| Service discovery | Instances come and go; callers need current addresses | Service discovery |
| API gateway | One front door for auth, rate limits and routing | API gateway |
| Traces, correlated logs, metrics | One user request crosses many processes | Observability |
| Contract tests, versioned APIs | Teams deploy separately; interfaces must not break silently | Schema evolution |
| Service-to-service auth | The internal network is no longer a trust boundary | mTLS |
A sidecar or service mesh can supply some of this without code changes, at the price of another moving part. None of it ships a feature.
Conway’s law: an organisational choice
In 1968 Melvin Conway observed that an organisation designing a system produces a design that copies its own communication structure. Teams split by layer produce a layered system in which every feature needs every team; teams split by business capability produce services along those lines.
Fowler goes as far as calling microservices primarily a tool to structure a development organisation. A dozen or two people can talk informally and will naturally build a monolith, and that is fine. The pressure comes from coordination: possible conversations among n people grow as n(n − 1)/2, so 8 people have 28, 50 have 1,225 and 150 have 11,175. Services with clear owners replace most of those conversations with an API contract.
The inverse Conway manoeuvre runs the law backwards: form small, long-lived teams around the capabilities you want as services, and the architecture follows, though only if code and teams change together.
Two rules of thumb:
- A team may own several services; each service has exactly one owning team. Shared ownership is how services quietly couple again.
- Watch services per engineer. Fowler has seen a team of 60 running 20 services and a team of 4 running 200, which is 50 services per person to deploy, patch and watch.
Choosing: monolith first, or not
Fowler’s Monolith First observation: almost every successful microservice story he heard began with a monolith that grew too big and was broken up, while almost every system built as microservices from scratch ended in serious trouble. Two reasons:
- You don’t yet know if the product works. Early on, fast feedback matters most and the premium is drag.
- You don’t yet know the boundaries. Moving behaviour between services is far harder than between folders, so find the seams in one codebase first (finding service boundaries with DDD).
Variants include peeling services off the edges of a quiet core, treating version one as sacrificial, or starting with two or three coarse services and splitting them as boundaries settle.
Starting split can still be right when you are replacing a well-understood system, when you have several teams from day one and people who have run microservices before, or when one part has radically different needs, such as a GPU-heavy transcoder or a card-data component you want in a small compliance scope. Even then the result is “a monolith plus a few services”, not forty. Uber’s advice matches: in small organisations the operational benefit likely doesn’t offset the complexity. For the wider menu, see choosing an architecture style.
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Deployable units | One | One | One per service, tens to thousands |
| Module boundaries | Convention; erode with growth | Enforced by a CI check | Enforced by the network |
| Call between parts | In-process, nanoseconds | In-process, nanoseconds | About 0.5–1 ms, and it can fail |
| Transactions | One ACID transaction | One ACID transaction | Local transactions, sagas, outbox |
| Scaling | Copy the whole app | Copy the whole app | Per service, hardware per workload |
| Fault isolation | One bug can take all down | Same process, same blast radius | Per service, if callers degrade |
| Release cadence | One release train | One train, faster per-module tests | Each team ships alone |
| Platform needed | One pipeline, monitoring | Same plus boundary checks | Pipelines, discovery, tracing, gateway |
| Suits | One to a few teams | Up to hundreds of developers | Many autonomous teams |
| Main risk | Big ball of mud | Boundary discipline lapses | Distributed monolith, ops overload |
What real teams learned
Segment: 140+ services back into one (2018)
Segment forwards customer events to more than a hundred “destinations” such as analytics tools. One shared queue let a single slow destination delay everyone, so each destination got its own service, queue and later repository. That fixed the blocking, then buried a small team: 50 new destinations meant 50 new repos, shared libraries drifted to different versions, every service needed its own autoscaling tuning, and three full-time engineers spent most of their time keeping the system alive.
They merged 140+ services into one, with a new system, Centrifuge, taking over the per-destination queueing that provided isolation. One engineer could deploy in minutes, and shared-library improvements rose from 32 under microservices to 46 the following year. The price: one bad destination can crash the service for all, and in-memory caches spread over 3,000+ processes run colder.
Lesson: the services existed for isolation. Once isolation came from elsewhere, the per-service tax wasn’t worth paying.
Shopify: a modular monolith, enforced by tooling (2019–2020)
Shopify’s Rails monolith has had more than a thousand contributors and, by 2020, over 2.8 million lines of Ruby. By 2016 a harmless change could trigger cascades of unrelated test failures. The team weighed microservices and rejected them for the pipelines, infrastructure, latency and cross-service refactors they would bring.
Instead, from 2017 they sorted about 6,000 classes into components by business domain in one script-generated pull request, then built tools to stop components reaching into each other. The first recorded call graphs during tests and took over an hour. Its successor, Packwerk, checks static references in a few minutes, so it runs on every pull request; it has since been open-sourced. By September 2020 the monolith had 37 components, about a third enforced by Packwerk, and the team had swapped out its legacy tax engine, which they say would once have been nearly impossible.
Lesson: you can get most boundary benefits without the network, but only if a tool fails the build.
Prime Video: one monitoring service goes single-process (2023)
Mind the scope: one internal tool, the audio/video quality-monitoring service, not Prime Video as a whole. Its first version was serverless: AWS Step Functions orchestrated a media converter and defect detectors on AWS Lambda, with video frames passed through S3. It hit a hard scaling limit at about 5% of the expected load, because orchestration made several state transitions per second of every stream (hitting account limits, billed per transition) and frames meant a very high number of S3 calls.
The team kept the same components but packed them into one process on Amazon ECS, passing frames in memory. Infrastructure cost fell by over 90%, and the service now monitors thousands of streams.
Lesson: when components exchange large data at high frequency, the boundary itself can dominate cost. Granularity should follow data flow (see serverless architecture).
Uber: too many services, so group them (2020)
By 2020 Uber ran about 2,200 critical microservices, and the costs were plain: one incident investigation spanned about 50 services owned by 12 teams, and simple features crossed many teams’ services. Going back wasn’t an option, so it introduced Domain-Oriented Microservice Architecture (DOMA): related services grouped into domains behind one gateway, domains arranged in five layers that only depend downwards, and extension points so teams add logic without editing each other’s code. The 2,200 services were classified into 70 domains; platform onboarding time fell by 25–50%, and one integration dropped from three days to three hours. With half its microservices churning every 1.5 years, gateways let two large platform rewrites happen without migrating hundreds of callers.
Lesson: at very large scale, the unit you manage is a domain with one front door, not each microservice.
Migrating without a big-bang rewrite
If you do need to split, extract services along seams that already exist:
- Modularise first. Give each module a public interface and private tables, enforced in CI. Moving a boundary now costs a refactor, not a data migration.
- Pick the first seam by need: a different scaling profile, a team blocked by the release train, or a failure to contain.
- Route around the monolith. Put a gateway in front and apply the strangler fig: one route to the new service, everything else to the monolith, traffic shifted with a canary.
- Move the data. Give the service its own store, sync it with change data capture, switch reads, then writes, then drop the old tables (online data migrations).
- Repeat, or stop. “A modular monolith plus a few services” is a valid end state.
The full playbook, including the anti-corruption layer, is in strangler fig and anti-corruption layer.
Don’t split the code and leave the data shared
The most common failed migration moves the code into a service but leaves it reading and writing the monolith’s tables. Now two deployables are coupled through a schema neither team can change alone. Move data ownership with the code, even if it takes a second phase.
Going deeper: failure modes
Distributed monolith smells
- Releasing service A requires releasing B and C the same day.
- Several services read and write the same tables.
- A user request needs five or more synchronous hops and fails if any is down.
- A shared library holds business logic, so every service must upgrade together.
The shared database
Sharing a database is the commonest road to a distributed monolith: every schema change becomes a cross-team negotiation, one service’s heavy query slows the rest, and nobody knows who owns a column. At minimum give each service its own schema and credentials that can’t touch other schemas; separate instances where load demands it. Others get data through an API or events. One overloaded store hurts even inside a monolith; see monolithic persistence.
Chatty services and data ownership
Services that need many calls to do one thing usually have a boundary in the wrong place: watch for loops calling another service once per item. Fix it with batch endpoints, a read-only copy fed by events, or by merging the services. Every fact needs exactly one owner; copies elsewhere are read-only and lag. If two services must change the same record atomically, they probably belong together.
Versioned APIs and contracts
Independent deploys only work if interfaces evolve compatibly. Add fields; never repurpose or remove one without a deprecation window, and have readers ignore fields they don’t know. Consumer-driven contract tests in the provider’s pipeline catch a breaking change before it ships. Cut a new version only when you must break compatibility, and run both until callers move.
How many services per team
Every service carries fixed costs: a pipeline, dashboards, alerts, on-call, upgrades and patches. A team of six owning 30 services spends much of its week on upkeep. Aim for a handful per team, each owning a real business capability.
Cheat sheet
| Signals you’re ready to split | Signals you split too early |
|---|---|
| Several teams queue behind one release train | A small team owns many services and lives on upkeep |
| A module’s load or hardware needs differ sharply | Most features touch three or more services |
| Failures in one area keep breaking unrelated features | Services share tables or must deploy together |
| Full CI takes hours and fails for unrelated reasons | User requests make five or more synchronous hops |
| Stable domain boundaries are already enforced in code | You can’t trace one request end to end |
| Automated deploys, monitoring and on-call already exist | Cross-service data is fixed with hand-written scripts |
| Remember | Number or rule |
|---|---|
| In-process call vs network hop | Nanoseconds vs roughly 0.5–1 ms (illustrative) |
| 10 services at 99.9% in series | 0.999^10 ≈ 99.0%: about 7.2 h of failure a month, not 43 min |
| Fan-out to 10, each slow 1% of the time | 1 − 0.99^10 ≈ 9.6% of requests hit a slow call |
| Coordination paths among n people | n(n − 1)/2 |
| Transactions across services | Saga + outbox + idempotent consumers |
| Default answer | Modular monolith, plus a few coarse services with a reason each |
How to say it in the interview
“For a food delivery app I’d start with a modular monolith: modules for orders, restaurants, payments and delivery tracking, each owning its tables, boundaries checked in CI. I’d pull out services only with a concrete reason. Rider location pings are a write firehose that scales differently, so ingestion is a service. Payments is isolated for compliance and its own on-call. Notifications sit behind a queue. That’s four or five services, not forty. A user request makes at most two synchronous hops; the rest flows through events published with an outbox. As teams multiply, I’d split further along module boundaries using the strangler fig.”
How do you do a transaction across services? First avoid needing one: keep data that must change atomically in one service. Otherwise use a saga of local transactions with a compensating action per step, events published through a transactional outbox, and idempotent consumers. Two-phase commit blocks when the coordinator fails, so keep it for rare cases.
How do services find each other? Through a registry kept current by health checks. With client-side discovery the caller looks up healthy instances and load-balances itself; with server-side discovery it calls a stable name (DNS, a load balancer or a Kubernetes Service) and the platform routes. External clients come in through an API gateway.
How do you avoid cascading failures? Give every call a timeout shorter than the caller’s own budget, retry only idempotent calls with backoff and jitter, and wrap dependencies in circuit breakers and bulkheads. Keep synchronous chains short, shed load when queues grow, and let non-critical failures degrade the page instead of breaking it.
How big should a service be? Big enough to own a business capability and its data, small enough for one team to own and deploy alone. If two services always change together, merge them.
Why not microservices from day one? You’d pay the premium before knowing whether the product works or where the boundaries are, and moving a boundary between services costs far more than between modules. The exception is replacing a well-understood system with experienced teams.
Where to go next
- Choosing an architecture style: where these sit among event-driven, serverless and other styles.
- Finding service boundaries with DDD: bounded contexts, and cutting services that don’t need to chat.
- Distributed transactions: 2PC vs sagas: orchestration, choreography and compensation.
- Transactional outbox: publishing events without losing or inventing them.
- Service discovery: registries, health checks and lookups.
- Circuit breaker: stopping one failing service from taking its callers down.
- Sidecar, ambassador and service mesh: moving retries, mTLS and telemetry out of code.
- Observability: following one request across many services.
References and further reading
- Microservices: a definition of this new architectural term (Lewis and Fowler, 2014)martinfowler.comThe defining description and its nine characteristics, the warning about naive in-memory-to-RPC conversion, and the “multiplicative effect of downtime” sidebar.
- MonolithFirst (Fowler, 2015)martinfowler.comWhy most successful microservice systems began as monoliths, the ways to run a monolith-first strategy, and the counter-argument.
- MicroservicePremium (Fowler, 2015)martinfowler.comThe extra cost of running a suite of services, the “too complex to manage as a monolith” guideline, and team-to-service ratios seen in practice.
- MicroservicePrerequisites (Fowler, 2014)martinfowler.comRapid provisioning, basic monitoring, rapid deployment and a DevOps culture, plus tracing once you pass a handful of services.
- Conway’s Law (Fowler)martinfowler.comConway’s statement, the inverse Conway manoeuvre and its limits, and microservices as a tool to structure a development organisation.
- Goodbye Microservices: From 100s of problem children to 1 superstar (Segment, 2018)twilio.comWhy Segment split destinations into 140+ services, what it cost a small team, and the trade-offs of merging them back into one.
- Deconstructing the Monolith (Shopify Engineering, 2019)shopify.engineeringWhy Shopify chose a modular monolith over microservices, the componentization project and its first boundary-checking tool.
- Under Deconstruction: The State of Shopify’s Monolith (Shopify Engineering, 2020)shopify.engineeringCodebase size, 37 components, and the move from runtime call-graph checks to Packwerk’s static checks on every pull request.
- Scaling up the Prime Video audio/video monitoring service and reducing costs by 90% (Prime Video Tech, 2023, archived)web.archive.orgThe scope of the change (one quality-monitoring service), the Step Functions and S3 bottlenecks, and the over-90% infrastructure cost reduction.
- Introducing Domain-Oriented Microservice Architecture (Uber Engineering, 2020)uber.comWhy Uber adopted microservices, the costs at about 2,200 services, and DOMA’s domains, layers, gateways and extensions.