Cruxly

Cruxly › System design › Advanced Trade-offs

Monolith, Modular Monolith or Microservices: a Trade-off Analysis

the architecture question as a costed decision: what distribution charges, what one deployable charges, and the trigger that flips the answer

Open the interactive lesson

Most architecture arguments are opinions. This lesson turns "which architecture?" into a decision you can defend: what microservices cost in availability, latency, consistency and people, what a monolith costs in coupling and contention, what a modular monolith buys, and which facts about your organisation should decide. It does not re-teach where to cut a system; that is Decomposing a System into Services. This is the step before it: whether to cut at all.

A decision is a trade-off table, not an opinion

An architecture choice is defensible when you can name what you pay, what you get, and the fact that would change your mind. "Microservices are modern" and "monoliths are legacy" are both slogans. The staff-level move is to treat the question as a costed comparison with inputs you can measure.

There are three real options, not two:

Martin Fowler's "Microservice Premium" post (13 May 2015) frames the whole debate as a premium: microservices add complexity of their own (automated deployment, monitoring, failure handling, eventual consistency), and that premium only pays when the system is complex enough that a monolith has become too big to modify and deploy. His guideline in that post is not to consider microservices unless the system is too complex to manage as a monolith. The Azure Architecture Center's microservices page (dated June 2025) lists the same two columns: benefits such as independent deployment, scaling, fault isolation and small teams, against challenges such as complexity, testing, network latency, data integrity, versioning and the skills the team needs.

:::key Verified November 2026 Fowler, "Microservice Premium", 13 May 2015, and "MonolithFirst", 3 June 2015. Azure Architecture Center, "Microservices architecture style", page dated 30 June 2025. Both are opinion and guidance, not measurements; the numbers in this lesson are either from a company's own post, named as such, or labelled arithmetic. :::

The shape of the answer. Costs of distribution are paid on every request and every release by everyone. Benefits of distribution are collected only by the parts of the system or the teams that actually need independence. So the decision is about concentration: how much of the system needs independence, and is it worth taxing the rest.

What experts say in the room. "What is the problem a network boundary solves here?" If nobody can name one (a team blocked, a component that must scale alone, a data class that must be isolated), the boundary is a cost with no benefit. What a senior checks: that the proposal states the trigger for the next change of architecture, in either direction, before it is built.

What distribution costs: latency, consistency, operations

Every network hop is a tax on availability, on latency, on consistency and on people, and the first two can be computed before you build. What follows is arithmetic and stated assumptions, not any company's measurements.

Availability multiplies. If a request needs ten services in series and each is available 99.9 percent of the time, and failures are independent, the request succeeds with probability 0.999^10 = 0.990 (99.0 percent). A year has 8,760 hours: 0.1 percent downtime is 8.8 hours, while 1.0 percent is about 87 hours. At twenty services in the chain, 0.999^20 = 0.980, about 174 hours. The same code in one process had one availability number, not ten. Retries, fallbacks and caching can claw some of it back, but each is more machinery (see SLAs, availability maths and error budgets and Overload, load shedding and cascading failure).

Tail latency multiplies. Suppose each call exceeds its own 99th percentile 1 percent of the time. A request that makes 10 calls will see at least one slow call with probability 1 - 0.99^10 = 9.6 percent; with 30 calls it is 26 percent; with 100 it is 63 percent. At 100 calls, most requests meet at least one dependency's slow tail, so the tail stops being rare. Add a fixed cost per hop (serialisation, TLS, a proxy, queueing): an assumed 1.5 ms per hop is 15 ms across ten serial hops, on top of the work itself, versus nanoseconds for in-process calls.

Consistency gets weaker. In a monolith a business operation can be one database transaction. Across services each one owns its data, so, as the Azure page puts it, the change is unlikely to be one ACID transaction and leans toward eventual consistency. The tools for this (outbox, sagas, idempotency) are covered in Event-Driven Architecture: Choreography, Orchestration and Sagas and Service-to-Service Communication: Discovery, Mesh, Identity and Compatibility; the cost of adopting them is what to count here. Every cross-service invariant becomes code, a monitor and a runbook.

Operations scale with the service count. Each service needs a build pipeline, deployment config, dashboards, alerts, secrets, a database and its backups, an on-call owner and a minimum of two running instances. Assume, as an illustration and not a published figure, 1.5 engineer-days per month of fixed upkeep per service. At 40 services that is 60 engineer-days a month, about three full-time engineers (at 20 working days each), before any feature work. A platform team can reduce the per-service figure but cannot make it zero. The number of possible pairwise integrations also grows as n(n-1)/2: 10 services give 45, 20 give 190, 40 give 780.

CostMonolith40 microservices (illustrative arithmetic)
End-to-end availability, chain of N services at 99.9% eachone number, 99.9%0.999^N: 99.0% at N=10, 98.0% at N=20
Chance a request hits some call's p99single9.6% at 10 calls, 26% at 30
Business operation atomicityone transactionsagas and compensations
Upkeep at an assumed 1.5 eng-days per service-monthnear zero marginalabout 60 eng-days per month
Possible service-to-service linksnoneup to 780

Testing and cognitive load. A change that crosses services cannot be tested by one unit-test run. You need contract tests, a shared test environment or production-like staging, and a way to run a subset of the system locally. The engineer's mental model has to hold which service owns which fact, which calls are synchronous, and what happens when each is down. This is the cost that is hardest to measure and that most often decides whether a small team is productive.

What experts say in the room. "Each hop must buy something." The numbers are 0.999^10 and the n(n-1)/2 link count. The failure is the long synchronous chain that makes a 99.9 percent promise unachievable by arithmetic. What a senior checks: the longest synchronous call chain on the critical path, the per-service upkeep a platform team really delivers, and whether any business invariant now spans two stores.

What a monolith costs: coupling, deploy blast radius, team contention

A monolith's cost is not slowness; it is that everything is coupled by default and nothing stops it. Fowler's "Microservice Premium" is fair to monoliths: he disputes the claim that they inherently block continuous delivery, and cites large companies that deliver quickly from one codebase. The honest list of costs is shorter and more specific than the slogans.

The key insight is that these costs have different cures. Coupling is cured by module boundaries and a build rule. Deploy blast radius is partly cured by feature flags, canaries and fast rollback. Team contention is cured by ownership of modules and a fast pipeline. Only a few costs truly need a process boundary: independent scaling of a very different workload, hard fault isolation, a regulatory wall, or a technology that cannot live in your runtime.

:::warn Equating monolith with spaghetti When a monolith is a mess, the cause is usually missing boundaries, not the single deployable. Splitting a tangled monolith into services keeps the tangle and adds a network between its strands; this is what Decomposing a System into Services calls a distributed monolith. :::

What experts say in the room. "What exactly hurts?" The argument is to list the pain, then match each pain to the cheapest cure: a lint rule, a flag, a faster pipeline, or a process boundary. The failure to avoid is prescribing the most expensive cure for a cheap problem. What a senior checks: deploy frequency and lead time per team, the percentage of builds blocked by another team's failures, and the count of cross-module imports that bypass a public interface.

The modular monolith as the middle path

A modular monolith keeps one deployable and adds what a monolith lacks: boundaries that the build enforces. Fowler's "MonolithFirst" (3 June 2015) names this as the first of two approaches for a new system: design the monolith with strong modularity at the API boundaries and in data storage, so it can be split later. He also says he would feel more comfortable with this route if he had heard a decent number of stories where it worked out, and that most systems acquire too many dependencies between their modules to be sensibly broken apart. Boundaries only count if something enforces them, which is the point of the build rule below.

Shopify's engineering post "Under Deconstruction: The State of Shopify's Monolith" (16 September 2020, Philip Muller) is the clearest published account. Shopify says its core monolith had over 2.8 million lines of Ruby and 500,000 commits, developed since at least 2006 by hundreds of developers. Its stated aim is that developers should feel they work on a much smaller app than they really do, by dividing the code into components organised around commerce subdomains. It enforces this with Packwerk, a static analysis that finds references across component boundaries and runs on pull requests so that a change breaking the dependency graph or a component's encapsulation is rejected, plus Sorbet type signatures to define the contracts at component boundaries. The post says that at that time about a third of the 37 components used Packwerk, and that an earlier runtime call-graph tool (Wedge) was dropped because its output was noisy and the analysis took over an hour. Shopify also says it splits out services only for good reasons, because moving from one application to a distributed system increases overall complexity considerably; its examples are storefront rendering, a high-throughput read-only workload that scales on its own, and credit-card vaulting, where sensitive data should not flow through the rest of the system.

What the middle path buys.

PropertyPlain monolithModular monolithMicroservices
Deployables11many
Call between modulesdirect, unrestrictedvia declared interface, build-checkednetwork call
Transaction across modulesyesyes, but limit it by designsaga
Data ownershipshared tableseach module owns its tables by convention or schemaeach service owns its store
Independent releasenonoyes
Independent scale or isolationnonoyes
Cost of getting a boundary wronghigh, silenta refactor in one repocross-team migration

The last row is the strongest argument. Moving a boundary inside one repository is a refactor with compiler help and one atomic commit; moving it between services is a migration with two deploys, a data move and a compatibility period. Fowler's second reason for monolith-first is exactly this: boundaries are hard to move once locked into services, so find them while they are cheap to change.

How to make a boundary real. A module exposes a small interface (a facade or a set of types) and keeps everything else private; a static check or build rule fails any import or constant reference that crosses it; each module owns its tables, and a lint rule or schema separation prevents cross-module joins and foreign keys; cross-module communication that does not need a reply goes through in-process events, which later become a real queue. Do these and a later extraction is mostly mechanical: the interface becomes an API and the tables move to a new store.

What experts say in the room. "Boundaries without enforcement are comments." The number to cite is Shopify's scale, hundreds of developers in one deployable. The failure is a module system on paper: cross-module imports creep back in and the extraction later turns out to be a rewrite. What a senior checks: that the boundary rule runs in CI and blocks merges, the count of allowed exceptions and whether it shrinks, and that each module owns its tables.

Real moves in both directions

Companies move toward services and away from them, and the published reasons are specific, not fashionable. Read each in the company's own words and note what it did not claim.

Segment, consolidating back (Alexandra Noonan, 10 July 2018). Segment's post says its pipeline receives hundreds of thousands of events per second and forwards them to over 100 types of server-side destination. It had one service per destination. The post names the pain: a single shared queue suffered head-of-line blocking, because when one destination slowed, retries flooded the queue and delayed all; shared libraries drifted across the destination repositories (over 50 new destinations meant 50 new repositories), so a change could mean redeploying dozens of services; load varied from a few events a day to thousands a second, so small services had to be scaled by hand during spikes; and each new destination, about three a month, added a repository, a queue and a service. At the low point, the post says, three full-time engineers spent most of their time keeping the system running. Segment moved the destinations into one monolithic service fed by a new component called Centrifuge, planned to consolidate the more than 140 services into one, on a single version of each of its 120 unique dependencies, and built a traffic recorder so destination tests replayed recorded HTTP responses instead of calling live endpoints. The post reports that the test suite for those destinations went from taking as long as an hour to milliseconds, that shared-library improvements rose from 32 in the microservice period to 46 in the year after the move, and that one engineer could deploy in minutes.

The post also lists what it gave up: fault isolation (a bug that crashes the service takes down every destination), cache effectiveness (in-memory caches spread over more than 3,000 processes are hit less often), and the risk that a dependency upgrade breaks several destinations at once, which it mitigated with its automated tests. What the post does not say is that microservices are wrong; the lesson is that per-destination services with highly similar code, one team and uneven load paid the tax for little independence. Note also that the post is from 2018 and Segment's later architecture is not described by it.

Shopify, staying modular (16 September 2020). As above: a large codebase, many developers, and boundaries enforced with Packwerk, with services extracted for specific reasons such as independent scaling of a read-heavy workload and isolation of sensitive card data. This is a move not made: the evidence of a company at hundreds of developers that chose enforced modularity over wholesale distribution.

The move toward services. Fowler's "MonolithFirst" notes that the successful microservice stories he knew had started as a monolith that grew too large and was then broken up, while systems built as microservices from the start often ran into serious trouble; he states his evidence is thin and the advice tentative, and he records the counter-argument that starting with services builds team habits early if the team has done it before. Treat it as a hypothesis from a thoughtful practitioner, not a law.

:::key Verified November 2026 Segment, "Goodbye Microservices", published 10 July 2018 (canonical URL now redirects to twilio.com); Shopify Engineering, 16 September 2020; Fowler, 3 June 2015. The Segment and Shopify figures are each company's own claims as of those dates; both architectures may have changed since. :::

How to migrate in either direction.

DirectionSafe pathThe step that goes wrong
Monolith to servicesEnforce module boundaries first, extract one module behind its interface, strangle traffic over (see Decomposing a System into Services)Shared tables left behind, so the new service still depends on the old schema
Services back to fewer deployablesMerge services that always change and deploy together; unify the repository and dependency versions first, then the runtime, then the dataMerging runtimes without isolating failure domains, so one bug now has a larger blast radius
EitherMove traffic in slices with a rollback, keep the contract testsA big-bang cutover with no way back

What experts say in the room. "Reversibility is part of the design." Segment's numbers (140 services, 120 dependencies, 32 to 46 library improvements) show that consolidation is a project with a plan, not a revert. The failure is treating the first decision as permanent. What a senior checks: which services always deploy together (candidates to merge), the cost of the rollback path of any migration step, and whether the stated reason for a move is measured or assumed.

Team size and ownership as an input

The decision factors are not equal; team structure comes first, and traffic usually comes last. In rough order of how often they should decide:

  1. Team count and release coupling. Fowler's "Microservice Premium" lists large teams as the first source of complexity and applies Conway's law: organisations split into small groups to get a modular structure. The Azure page makes the same point from the other side: a microservice should be small enough for one feature team to build, test and deploy, and large teams are less productive because communication is slower. It is the number of teams that cannot release independently that matters, not the number of engineers alone.
  2. Rate and independence of change. If different parts change at very different cadences (a pricing engine weekly, a ledger quarterly), separate release trains help. If everything changes together, services only add coordination.
  3. Independent scaling needs. A component with a very different resource profile (a GPU job, a read-only high-throughput path like Shopify's storefront rendering) justifies a boundary even in a small organisation.
  4. Fault isolation. If a feature's failure must not take down checkout, isolate it. Be sure the isolation is real: the Azure page notes isolation holds only if upstream services handle faults correctly.
  5. Data ownership. If a data set has a distinct lifecycle, retention or consistency need, it favours a boundary. If two modules need to read and write the same rows in one transaction, they probably belong in one unit.
  6. Regulatory or security isolation. A wall that must keep sensitive data (such as card data, per Shopify's vaulting example) away from the rest of the system is a strong reason for a separate deployable.

Fowler also warns that team size alone does not settle it: he contrasts a team of 60 with 20 services and a team of 4 with 200, and says it is not clear how service size affects the premium. Use team count as the first input, not as a formula.

A decision table an interviewer will accept.

SituationDefaultWhy
Early product, under about 10 engineers, boundaries unclearMonolith, or modular monolithSpeed and learning; boundaries are cheap to move now
Several teams, one product, shared domainModular monolith with module ownershipContention is cured by ownership and a fast pipeline, not by a network
One component with a very different scale or hard isolation needModular monolith plus that one servicePay the tax only where it buys something
Many autonomous teams, stable boundaries, platform tooling in placeMicroservicesIndependence outweighs the tax, and boundaries are known
Compliance wall around one data classSeparate deployable for that classThe boundary is the requirement
Services that always deploy togetherMerge themThey are one unit with a network in the middle

What experts say in the room. "Draw the team boundary first; the architecture follows or fights it." The failure is services cut to a diagram that no team owns, which leave the work as coordinated as before plus a network. What a senior checks: the ratio of services to teams (a service with no owner is a liability), whether a typical feature touches one team's services or three, and whether on-call exists per service.

Writing the decision down: an architecture decision record

A decision that is not written down gets re-argued or silently reversed. An architecture decision record (ADR) is a short document with a fixed shape: context, decision, alternatives considered, consequences, and the trigger to revisit. Its value in this topic is that it forces the cost to be named at the moment the choice is made.

A useful ADR for this decision has these parts:

An example trigger (an assumption, not a benchmark). "If more than 20 percent of builds in a month fail because of another team's change, isolate that module's CI first; if it still blocks others, extract it." That tries the cheap cure before the expensive one.

:::warn Triggers without owners A trigger such as "revisit when traffic grows" with no owner and no metric never fires. Name who watches the metric and where it is graphed. :::

What experts say in the room. "What would make you change your mind?" is the interview's strongest question, and the ADR is the written answer. What a senior checks: that every ADR in this area has a cost accepted, a measurable trigger and a date.

Interview script

Say it in four moves: context, default, trigger, accepted cost. This is how a staff engineer compresses the whole analysis into a minute and survives the follow-ups.

  1. Context. "I'd want the team count, how often they block each other, the scaling profile, any isolation or compliance wall, and how stable the domain boundaries are." Ask two or three of these; the answers decide.
  2. Default. "Absent other facts I'd start with a modular monolith: one deployable and database, module interfaces enforced in the build, each module owning its tables." Add the arithmetic when asked why: "Ten synchronous hops at 99.9 percent is 99.0 percent end to end, roughly 87 hours of downtime a year versus 8.8."
  3. Trigger. "I'd extract a module when it needs independent scaling, a regulatory wall, or when its release cadence blocks other teams. That's the same reasoning Shopify gives for pulling out storefront rendering and card vaulting."
  4. Cost accepted. "One deployable means shared blast radius and a deploy that touches everyone; I'd contain that with feature flags, canary releases and module ownership. And I'd keep the decision reversible: Segment merged more than 140 services back into one when per-service overhead exceeded the independence it got."

Follow-ups to be ready for.

Do not answer with a technology list, recite "loose coupling" without naming a cost, or present microservices as free. Do name numbers, the trigger, and the cost you accepted.

Further reading: Neal Ford, Mark Richards, Pramod Sadalage and Zhamak Dehghani, Software Architecture: The Hard Parts (chapters 4 Architectural Decomposition, 7 Service Granularity and 15 Build Your Own Trade-off Analysis); Sam Newman, Building Microservices, 2nd edition. Primary sources: Fowler's "MonolithFirst" and "Microservice Premium" (2015), Segment's "Goodbye Microservices" (2018), Shopify Engineering's "Under Deconstruction" (2020) and the Azure Architecture Center microservices page.

How to say it in the interview

"My default is a modular monolith: one deployable, one database, hard module boundaries enforced in the build. Distribution is a cost I pay only for a reason, and the reason is almost never traffic; it is the number of teams that cannot release without each other. A chain of ten synchronous calls at 99.9 percent each is 99.0 percent end to end, about 87 hours of downtime a year instead of 8.8, so each hop must buy something. I would split out a component when it has its own scaling profile, its own regulatory boundary, or a team that is blocked by shared releases. The cost I accept is a harder fault-isolation story and a deploy that touches everyone, which I contain with module ownership, feature flags and a staged rollout."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons