Monolithic Architecture
Monolithic Architecture is the structural style in which an entire application is built, packaged, and deployed as a single executable unit — one binary, one container image, one Web Application Archive (WAR) file, or one process — with all of its modules communicating via in-process function calls rather than over the network. It is the default shape of nearly every successful software product when it begins, the architecture Martin Fowler and James Lewis explicitly endorsed as “MonolithFirst” in 2015 (Fowler 2015, MonolithFirst), and the architecture David Heinemeier Hansson defends as the “Majestic Monolith” (DHH 2016) for teams whose organizational scale does not yet demand a service-oriented split. Despite the cultural pressure exerted by the microservices movement of the 2010s, monoliths remain the correct default — Fowler’s “Microservice Trade-Offs” essay (2015) argues that the cost of distributed systems should not be paid until you have evidence that your monolith is failing along axes that microservices specifically remedy. Modern incarnations such as Shopify’s modular monolith (Shopify Engineering 2019) and the modulith pattern show that “monolith” is not a synonym for “spaghetti” — disciplined module boundaries inside a single deployable can deliver most of the benefits attributed to microservices at a fraction of the operational cost.
1. When to Use / When Not to Use
Choosing the architecture is the single highest-leverage decision in a project’s life cycle. The default should be a monolith; the burden of proof is on anyone proposing otherwise.
1.1 When a Monolith Is the Right Choice
A monolithic deployment unit is the right answer when any of the following hold:
- The team is small — fewer than roughly fifty engineers contributing to the codebase. With a small team, the coordination overhead of microservices (a separate repository per service, separate Continuous Integration / Continuous Deployment pipelines, separate observability dashboards, separate on-call rotations) outweighs the gains from independent deployability. Newman 2021 (chapter 1) explicitly suggests that microservices begin to help an organization above the headcount where a single team can no longer fit in one room.
- The product is in early discovery — the bounded contexts (in Domain-Driven Design Strategic Patterns terms) are not yet stable. Splitting prematurely along the wrong seams forces expensive re-shaping later. A monolith lets the design crystallize through ordinary refactoring before service boundaries are committed to Hypertext Transfer Protocol (HTTP) contracts.
- Throughput requirements are below the single-machine ceiling. A modern bare-metal server with two sockets, ~64 cores, and 512 GB of RAM can comfortably serve tens of thousands of requests per second for a typical web application. Stack Overflow has documented serving billions of requests per month from approximately nine web servers running a single .NET monolith (Craver 2016, Stack Overflow: The Architecture, 2016 Edition). If your projected load fits in this envelope, distributed-systems complexity is unjustified.
- You need transactional integrity across business logic. A monolith allows ordinary database transactions across all modules, because all modules share one database connection pool and call into one another as in-process function calls. Distributed transactions across microservices require either two-phase commit (see Two-Phase Commit — slow, fragile, and increasingly out of fashion) or the Saga Pattern (which is essentially a hand-rolled compensating-action protocol). When a single business operation must atomically update three tables, a monolith does it for free.
- Deployment velocity for the team is acceptable as a single artifact. If the team can release the entire application weekly without merge-conflict pain, a monolith is fine. If a single release blocks ten teams who all need to coordinate their changes, the monolith has become organizational glue and is starting to fail.
1.2 When a Monolith Is the Wrong Choice
The monolith stops being the right architecture when:
- The codebase has grown past the point where any one engineer can keep its mental model. The exact threshold depends on language, modularity discipline, and tooling, but in practice this happens around ~500K to a few million lines of code. Beyond it, code reviews lose context, refactors become risky, and the merge-conflict rate grows superlinearly.
- Independent scaling becomes economically meaningful. A monolith scales by replicating the entire deployable. If 95% of your traffic is on a search endpoint that is Central Processing Unit (CPU) bound and 5% is on an image-resize endpoint that needs a Graphics Processing Unit (GPU), you cannot economically allocate resources per module — you must replicate the whole thing on whichever hardware is most expensive. Microservices’ main mechanical benefit is per-service scaling.
- Polyglot persistence or polyglot programming becomes mandatory. A monolith typically lives in one language and talks to one database. If your domain genuinely needs a search engine for one bounded context, a graph database for another, and a Python-based machine-learning library for a third, the monolith starts straining (Polyglot Persistence; see Polyglot Persistence).
- Multi-region or strict failure-isolation requirements appear. A monolith’s blast radius is the whole application — a memory leak in the recommendation module crashes the checkout flow. Microservices, Bulkhead Pattern, and Cell-Based Architecture exist precisely to bound blast radius.
- Team boundaries solidify and Conway’s Law (see Conway’s Law) starts cutting against the architecture. When five product teams each “own” different parts of the same monolith, every release is a coordination event involving all five teams, and code ownership becomes ambiguous. This is the classic moment to consider extracting services along team lines.
2. Structure
flowchart TB Browser[User Agent / Browser / Mobile App] -->|HTTPS| LB[Load Balancer / Reverse Proxy] LB -->|HTTP| App1[Monolith Instance 1] LB -->|HTTP| App2[Monolith Instance 2] LB -->|HTTP| App3[Monolith Instance 3] subgraph App1Internal["Inside one Monolith process"] Web[Web Layer / HTTP handlers] Auth[Auth Module] Catalog[Catalog Module] Orders[Orders Module] Billing[Billing Module] Notifications[Notifications Module] DAO[Data Access Layer] Web --> Auth Web --> Catalog Web --> Orders Web --> Billing Web --> Notifications Auth --> DAO Catalog --> DAO Orders --> DAO Billing --> DAO Notifications --> DAO end App1Internal --> DB[(Single Relational Database)] App2 --> DB App3 --> DB
What this diagram shows. A typical web monolith deployed behind a load balancer with three identical replicas. Each replica is the same binary running the same code; the load balancer (nginx, HAProxy, or a cloud Layer-7 balancer) round-robins requests across them. Inside one replica, the modules are arranged as ordinary code packages — auth, catalog, orders, billing, notifications — communicating via direct method calls. The data-access layer fronts a single shared database. The critical observation: there is one deployable, one process per replica, and one database. Adding a feature means editing one repository, building one artifact, and deploying it to all three replicas (typically with a rolling update). Inter-module calls are nanosecond-scale function invocations, not millisecond-scale Remote Procedure Calls. This is the source of the monolith’s two great virtues — operational simplicity and call-path performance — and also its two great vices — coupled deployment and shared blast radius.
3. Core Principles
The inviolable rules that distinguish a monolith from “many services that happen to share a database” or “a single repository containing many services”:
- Single deployment unit. The artifact produced by Continuous Integration is one package — one container image, one Java Archive (JAR), one Python wheel-plus-dependencies, one Ruby Gem bundle. There is no concept of partial deployment. Either every module of the new version is running, or none is.
- Shared address space. All modules execute in the same Operating System (OS) process. Calls between modules are ordinary function calls, with no marshalling, no network hops, no serialization, and no possibility of partial failure (a remote call can succeed-on-the-other-side but fail-on-the-network; an in-process call cannot).
- Shared lifecycle. When the process starts, every module starts; when it shuts down, every module shuts down. There is no per-module deployment window, per-module restart, or per-module versioning at runtime.
- Single database (typically). A canonical monolith uses one database server (possibly with replicas for read scaling, but logically one database). All modules read and write through one schema. This is the property the Shared Database Anti-Pattern would be — if multiple services shared a database — but inside a monolith it is the normal state of affairs because the modules are not separately owned services.
- One repository (typically). Source code lives in a single Version Control System (VCS) repository. All modules are versioned together; a single commit can change three modules atomically. This is how the monolith earns the “atomic refactor” property — you can rename a function used in five places and update all call sites in one Pull Request.
These principles are the positive statement of what a monolith is. They are not “accidental properties to be tolerated”; they are the design that produces operational simplicity. A microservices-style architecture deliberately abandons each one in exchange for independent deployability and per-service scaling.
4. Request Flow
sequenceDiagram participant U as User Browser participant LB as Load Balancer participant M as Monolith Process (instance 2) participant Web as Web Layer participant Orders as Orders Module participant Billing as Billing Module participant DB as Database U->>LB: POST /api/orders {item, qty, payment} LB->>M: forward to instance 2 M->>Web: dispatch to OrdersController Web->>Orders: createOrder(userId, item, qty) Note over Orders: Same process — function call, no network Orders->>DB: BEGIN TRANSACTION Orders->>DB: INSERT orders ... Orders->>Billing: chargeCard(userId, amount) Note over Billing: Same process — function call Billing->>DB: INSERT charges ... Billing-->>Orders: charge_id Orders->>DB: UPDATE orders SET charge_id = ? Orders->>DB: COMMIT Orders-->>Web: order Web-->>M: HTTP 201 Created M-->>LB: response LB-->>U: 201 Created
What this diagram shows. A representative create-order request flowing through a monolith. The user’s HTTP request is balanced to one of the (identical) monolith instances. The Web Layer dispatches to a controller method, which calls into the Orders module via a regular method call. The Orders module opens a database transaction, calls into the Billing module — also a regular method call, in the same process, sharing the same database connection — which records a charge in the same transaction. The transaction commits atomically: either the order, the billing record, and the order’s charge_id reference are all persisted, or none are. There is no possibility of “order created but billing record missing” because there is no network in between, and the transaction spans both writes. Compare this with the equivalent in a microservices system, where Orders and Billing are separate services connected over HTTP or gRPC; that path would require a Saga Pattern or a Two-Phase Commit to achieve the same atomicity, with all the failure-mode complexity those patterns introduce. The monolith’s request path is, in a precise technical sense, simpler.
5. Variants
The word “monolith” covers a family of related-but-distinct architectures. The disciplined ones are evolutionary stepping stones; the undisciplined ones are anti-patterns.
5.1 Big Ball of Mud Monolith
Brian Foote and Joseph Yoder’s 1997 paper Big Ball of Mud (presented at PLoP ‘97; http://www.laputan.org/mud/) observed wryly that the de-facto standard software architecture — “the most frequently deployed” of them — is, in practice, no architecture at all: a sprawling, undocumented, organically grown codebase where any module can call any other and there are no enforced boundaries. This is the failure mode of the monolithic style, not the style itself. See Big Ball of Mud Anti-Pattern for the canonical write-up. Recognizing your monolith is a Big Ball of Mud is the precondition for fixing it; pretending otherwise prolongs the suffering.
5.2 Modular Monolith (Modulith)
The disciplined evolution: a single deployable, but with strictly enforced internal module boundaries. Each module exposes a small public Application Programming Interface (API) and hides everything else as private implementation. Cross-module calls go through the public API; the build system or a static analyzer rejects code that imports private internals. Shopify’s 2019 engineering essay Deconstructing the Monolith (Kirsten Westeinde, Shopify Engineering, https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity) describes how they retrofit module boundaries onto their Ruby on Rails monolith using a custom dependency-graph tool, treating module API violations as build failures. The key property: a modulith retains all of the monolith’s operational simplicity (one deployment, one process, transactional integrity) and gains most of the cohesion benefits of microservices (clear ownership, replaceable internals, suppressed coupling). Many systems that consider migrating to microservices should pause and migrate to a modulith first; the modular boundaries become the future service boundaries if a split eventually happens, and many teams discover the split is no longer needed once the boundaries are clean.
The disciplined modular-monolith doctrine — sometimes summarized as the rule that every module must be designed as if it were a future service — has the following operational consequences that go well beyond ordinary “package organization.” First, every cross-module call goes through a named, versioned public interface — typically a Ruby module, a Java package-info, a TypeScript barrel file, or a Go package — that is the only thing other modules may reference. Anything not on that interface is private implementation, and a static-analysis tool (Shopify’s packwerk for Ruby, ArchUnit for Java, dependency-cruiser for JavaScript, Modulith for Spring Boot, NetArchTest for .NET, import-linter for Python) fails the build when a forbidden import is added. The discipline is mechanical: a wrong import does not even compile, the way a missing import would not compile. Second, each module owns its persistence: a module’s tables are referenced only from inside the module, with cross-module reads going through the module’s API. Shopify implemented this by giving each module a separate database schema with module-level grants (Postgres GRANT SELECT ON SCHEMA orders TO app_user_orders style), so that a stray cross-schema query fails at the database, not just at code review. Third, each module emits domain events through a shared in-process event bus — events are recorded in an outbox table (see Outbox Pattern) so that they can later become inter-service messages without changing producer code. Fourth, ownership is per module: one team owns the catalog module, another owns the cart module, and the team-to-module mapping is documented in CODEOWNERS. The modular monolith is not “no architecture” — it has architecture-equivalent rigour, just within one process.
The Westeinde 2019 RailsConf talk Deconstructing the Monolith makes this explicit: Shopify started from a five-million-line Rails monolith with effectively zero internal boundaries and spent multiple years moving toward a modulith. The mechanical sequence she describes was (a) categorize every file in the codebase into a candidate module based on bounded-context analysis (this required deep involvement of product-team domain experts; the boundaries Shopify ended up with — orders, payments, inventory, pricing, merchant_dashboard, online_store, developer_platform, etc. — closely match the team boundaries by Conway’s Law); (b) build the static-analysis tool (packwerk) to detect cross-boundary violations and report them on every pull request; (c) declare initial boundaries in warning-only mode so the build did not break for every existing violation; (d) drive the violation count down month by month, treating each violation as technical debt to be cleaned up; (e) once a module’s violations were zero, flip the warnings to errors so future violations were impossible. The whole effort required an investment of multiple engineering quarters per module, sustained over a multi-year rolling program. The reported outcome: developer productivity per module improved measurably (less coupling means less coordination, fewer test-suite breakages from unrelated changes), onboarding into a single module became fast (a new engineer could become productive in inventory without learning all of orders), and Shopify did not pay the operational cost of microservices (one deployable, one process, one connection pool, atomic database transactions across modules where business semantics demanded it). The modulith preserved the monolith’s operational virtues while gaining the microservice’s organizational ones.
A particular detail in the Westeinde 2019 talk worth highlighting: Shopify discovered that their initial intuited module boundaries did not match the actual coupling structure of the code. The team-product mapping suggested boundaries like merchant, shopper, admin; the actual code coupling clustered around domain concepts like orders, payments, inventory, which cut across the team-product mapping. The boundary-discovery process was a months-long collaboration between the platform-architecture team and product teams to find boundaries that minimized cross-module dependencies. The lesson: module boundaries cannot be derived from organizational structure alone (Conway’s Law applied naively gives wrong answers); they emerge from the intersection of organizational structure with the domain’s natural coupling lines. This is the practical content of Domain-Driven Design’s bounded context concept (Evans 2003, Vernon 2013) — the bounded context is the correct unit of modulith partitioning because it is defined by both the team owning it and the domain’s natural conceptual boundaries.
A complementary discipline — explicit in Vernon’s 2013 Implementing Domain-Driven Design and elaborated in Drotbohm’s Spring Modulith documentation — is thinking about each module as a future service. Concretely: every public API call between modules must be designed as if it were a network call. That means it must be coarse-grained (a single call should accomplish one business operation, not require ten round-trips), it must accept and return values rather than mutable references, it must define explicit error semantics (success / failure / not-found) rather than throwing implementation-specific exceptions, and the data exchanged must use module-owned types rather than shared global types. The discipline produces modules whose public APIs already look like service APIs; if a module ever needs to be extracted, the extraction is a refactoring of implementation (the in-process call becomes an HTTP or gRPC call), not a redesign of interface. This is the property Spring Modulith (Drotbohm 2022+, https://docs.spring.io/spring-modulith/reference/) elevates to a first-class architectural framework: it provides annotations and runtime checks that enforce module boundaries at the Spring Boot level, and integration tests that simulate calls between modules to validate the interface in isolation, the way a microservice contract test would.
5.3 Distributed Monolith
The pejorative: the application has been split into multiple services, but the services are so tightly coupled — synchronous request chains, shared database, lockstep deployments — that they exhibit none of the benefits of microservices and all of the costs (network latency, distributed-systems failure modes, deployment coordination). Newman 2021 calls this “the worst of both worlds.” See Distributed Monolith Anti-Pattern for diagnostic criteria; the existence of this anti-pattern is a major reason Fowler and Newman both recommend monolith-first.
5.4 Self-Contained System (SCS)
A pattern documented at scs-architecture.org (2014–) — a monolith that owns one bounded context end-to-end (its own user interface, business logic, and database). A “system of systems” can then be composed of multiple SCSs, each itself a monolith, integrated mostly via Hypertext links and asynchronous events rather than synchronous Remote Procedure Calls. This sits between pure monolith and microservices: each SCS is internally monolithic, but the overall architecture is service-oriented at coarse grain.
5.5 Modulith with Outbox
A modular monolith augmented with a transactional Outbox Pattern so that domain events recorded inside a database transaction are reliably delivered to other systems (often other moduliths or downstream microservices) via a message broker. This is a frequent stepping stone toward eventual decomposition: the events that will travel between services in the future already travel between modules today, recorded durably in the outbox table. Splitting later becomes a matter of moving the consumer of an event from in-process to out-of-process, with the producer unchanged.
5.6 Worked Example: A 500K-Line E-Commerce Modulith
To make the modulith discipline concrete, consider a hypothetical e-commerce platform — call it MerchantGrid — built as a Ruby on Rails or Spring Boot monolith of roughly 500,000 lines of code. The application serves merchant storefronts, processes shopper orders, handles payment, manages inventory, and ships fulfilled orders. The team is around 80 engineers organized into six product squads. The application is one deployable, deployed twenty times a day via continuous deployment with feature flags, running across approximately 40 application-server replicas behind a Layer-7 load balancer with a single Postgres primary plus three read replicas. The architecture decisions:
The codebase is divided into six modules — catalog, cart, checkout, payments, fulfillment, account — each owned by exactly one squad. Each module lives in its own top-level directory (app/modules/catalog/, app/modules/cart/, etc.), with an internal split between public/ (the module’s API exposed to other modules) and internal/ (everything else). Cross-module imports of internal/ are forbidden by the build’s static analyzer, which fails the build if a file in app/modules/cart/ imports from app/modules/catalog/internal/. The only legal cross-module import is from another module’s public/ directory, which contains a small set of facade classes — Catalog::Public::ProductLookup, Catalog::Public::CategoryNavigator, and so on — each with a documented contract. The public/ files have a header comment that says “This is the catalog module’s external API. Treat as if this were a network service. All changes here require coordinating with consuming modules.”
Each module owns a Postgres schema. The catalog module owns the catalog.products, catalog.product_variants, and catalog.categories tables; the payments module owns payments.transactions, payments.refunds. The application database connection has multiple roles: app_catalog has SELECT, INSERT, UPDATE, DELETE on the catalog schema and only SELECT (or no access) on others; app_payments similarly. When the cart module needs to read product data, it calls Catalog::Public::ProductLookup.find_by_id(id) rather than running SELECT * FROM catalog.products WHERE id = ? directly. The lookup method is a normal Ruby (or Java) method call in the same process, costing roughly 100 nanoseconds; if cart later moves to a separate service, the same call becomes an HTTP or gRPC roundtrip costing about 1 millisecond — a 10,000× slowdown that can be selectively absorbed because the call is already coarse-grained.
Cross-module business operations use database transactions when atomic semantics matter. When a shopper completes checkout, the checkout module orchestrates: open a database transaction; call Inventory::Public::Reserve.run(items) to decrement stock; call Payments::Public::Charge.run(total, payment_method) to capture funds; call Cart::Public::Drain.run(cart_id) to empty the cart; insert an order row owned by checkout; commit the transaction. All four operations live in the same Postgres transaction; any failure rolls everything back atomically. The equivalent in a microservice architecture would require a Saga (Saga Pattern) — a multi-step compensation choreography that handles partial failures by undoing previously-completed steps. The Saga is harder to write, harder to test, and harder to reason about; the monolith’s transaction is free. This is one of the strongest arguments for the modulith and the most under-appreciated cost of premature microservice extraction.
Cross-module eventual communication uses an in-process event bus backed by an outbox table. When the checkout module successfully creates an order, it publishes an OrderPlaced event; the event is appended to an outbox.events table inside the same transaction as the order insert, so the event is durable iff the order is. A separate background poller in the same process reads from outbox.events and dispatches to in-process subscribers — fulfillment schedules a shipment, account updates lifetime spend, catalog decrements available inventory counts (eventually consistent display). All subscribers run in the same process, but the event semantics match what would happen in a microservice architecture: the producer doesn’t know who subscribes, subscribers run independently, failures in one subscriber don’t fail the producer or other subscribers. If account were extracted to a service, the outbox publisher would simply switch from in-process dispatch to publishing to a Kafka topic — producer code unchanged.
Performance characteristics of this architecture: an in-process method call between modules takes roughly 50–200 nanoseconds (function-call overhead plus a memory load); a network call to a different service takes roughly 0.5–5 milliseconds for in-data-center gRPC, or 5–50 milliseconds for a cross-AZ HTTPS call. The ratio is 4 orders of magnitude. A user request that traverses ten modules in a monolith spends ~1 microsecond in cross-module overhead; the same request in a microservices architecture spends 5–50 milliseconds in cross-service overhead. For latency-sensitive paths, this matters enormously; for batch or async paths, it usually does not. The modulith’s most under-appreciated property is that latency-critical paths get the in-process performance for free, because the cross-module call is a normal function call. Microservices architectures pay this latency tax universally and try to claw it back with caching, parallel fan-out, or read-side denormalization. The modulith simply does not pay the tax in the first place.
Operational characteristics: one Postgres database (with replicas), one application binary, one CI/CD pipeline, one observability dashboard per concern (latency, throughput, error rate), one on-call rotation. Adding a new module is creating a new directory and registering it with the build’s module list; it does not require provisioning new infrastructure, adding a new service to the mesh, or coordinating with the platform team. The team’s velocity is bounded by code-review and test-suite speed, not by inter-service contract negotiation. Per Westeinde 2019, this is the property that lets Shopify ship at the cadence it does despite a monolith of millions of lines: the operational cost of the monolith doesn’t scale with module count.
Concretely, the test pyramid for MerchantGrid looks like this: each module has its own unit-test suite (running in 30 seconds per module on a developer’s laptop); each module has integration tests that exercise the public API with the database schema mocked or rolled back per test (running in 2–5 minutes per module); the application has a small number of end-to-end tests running against the full stack (running in 10–15 minutes total, used as a smoke test in CI). The total CI time for a typical pull request is 5–10 minutes; for a multi-module change it is 15–25 minutes. The Spring Modulith framework formalizes this with @ApplicationModuleTest annotations that load only the module under test, dramatically reducing test-startup time vs full Spring Boot context tests. A microservices version of MerchantGrid would have this same test pyramid per service, plus a substantial cross-service integration-test infrastructure that the modulith does not need.
A subtlety often missed: the modulith’s transactional boundary is not necessarily the entire database transaction. Within the modulith, individual modules can choose to use shorter transactions (per-operation transactions for non-orchestrated calls) or longer transactions (multi-module orchestrated transactions for cross-module business operations). The choice is per-call, made by the orchestrating code, with the database engine providing the transactional primitives. The flexibility is what gives the monolith its consistency advantage: when the business semantics demand atomicity (charge the card and decrement the inventory and create the order, all-or-nothing), the orchestrating code wraps the cross-module calls in a transaction; when the semantics permit eventual consistency (the analytics module updates lifetime spend after the order is committed; failure of analytics shouldn’t roll back the order), the code uses the outbox-event pattern. The choice is explicit and code-visible, not buried in cross-service distributed-transaction infrastructure.
6. Real-World Examples
The “monolith is dead” narrative is, empirically, false. Major industry deployments running on monoliths include:
- Stack Overflow. Nick Craver’s 2016 architecture overview (Stack Overflow: The Architecture, 2016 Edition, https://nickcraver.com/blog/2016/02/17/stack-overflow-the-architecture-2016-edition/) describes a single .NET monolith serving Stack Overflow plus all Stack Exchange sites, backed by a small SQL Server cluster. The case is canonical because it shows that the supposed scaling ceiling of monoliths is, for a competently written application, very high. Craver’s series is the canonical deep writeup for any engineer interested in how a monolith actually scales: the 2016 entry is preceded by Stack Overflow: The Hardware, 2016 Edition and followed by How We Do Deployment and How We Do App Caching — together they form the most detailed publicly-available account of a high-traffic monolith’s architecture from someone who actually built and operated it. The numbers Craver reports merit attention, and they are routinely misquoted, so pin them precisely. On a representative day (February 9, 2016) the load balancers handled 209,420,973 HTTP requests and served 66,294,789 page loads — i.e. roughly 209 million HTTP requests and 66 million page loads per day, not per month (the per-month figure is therefore on the order of six to two billion, depending on which metric). The web tier was 11 IIS-hosted ASP.NET servers: 9 “primary” (numbered 01–09) plus 2 “dev/meta” staging servers. The headroom claim is striking and exact: Craver writes that the fleet was so overprovisioned that “we’re down to needing only 1 web server. We have unintentionally tested this, successfully, a few times … I’m saying it works. I’m not saying it’s a good idea.” That is roughly nine-fold headroom on the primary web tier, held for rolling builds, redundancy, and burst capacity rather than steady-state need. The monolith was backed by 4 Microsoft SQL Server machines (organized into two clusters with replicas, in a high-availability arrangement), 2 dedicated Redis servers, 3 tag-engine servers, 3 Elasticsearch servers, and 4 HAProxy load balancers (two added to support Cloudflare). Total: roughly two dozen production machines for a website serving traffic in the same league as the largest sites of the era.
The architectural disciplines Craver attributes the success to: (a) aggressive caching. Hot data lives in Redis with carefully-tuned expirations, with cache invalidation on writes; (b) read-replica scaling for the database. Reads are distributed across replicas while writes (and read-after-write paths) go to the primary; (c) avoidance of premature splits. When a feature is added, it is added to the existing monolith, not as a new service; the monolith’s internal architecture is layered (controllers → services → data access) but always within the single deployable; (d) ruthless performance discipline. Stack Overflow open-sourced and runs MiniProfiler, which exposes per-request timings, and treats latency regressions as bugs; (e) write-once-everywhere code reuse. The same C# codebase serves Stack Overflow, Stack Exchange, Server Fault, and the other Q&A network sites; multi-tenancy is handled by configuration and template selection within the monolith, not by separate deployments per site. The implication for system-design candidates: the supposed throughput ceiling of a monolith is, for competently engineered applications, far above what most companies will ever need. Quoting Craver in an interview signals that you have read source material rather than reciting microservices folklore. The blog series remains required reading.
-
Shopify. Shopify Engineering’s 2019 essay Deconstructing the Monolith (Kirsten Westeinde, RailsConf 2019; https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity) describes their large Ruby on Rails monolith, which serves a non-trivial fraction of all e-commerce traffic. Rather than splitting into microservices, Shopify invested in retrofitting internal module boundaries — moving from undisciplined monolith to modular monolith — and reports that this gave them most of the developer-productivity wins they would have hoped for from microservices, without the operational cost. The post-2019 follow-ups (Westeinde RailsConf 2019 talk; later writeups by Stephanie Telmar on Shopify’s
packwerkRuby static analyzer, https://github.com/Shopify/packwerk) document the operational outcomes: the modular monolith now has dozens of explicit modules with publicly-declared APIs, build-time enforcement of module-boundary rules, and per-module ownership. Shopify reports that internal-module test suites run in seconds rather than minutes (because module-level changes only need to test that module’s API contracts), and that engineers can become productive on a single module without learning the rest of the monolith. The implication: the modulith is what most “we should microservices this” projects actually need, and the cost is incremental rather than wholesale. -
GitHub. GitHub.com is, to this day, a single Ruby on Rails monolith — and this is officially confirmed by GitHub’s own engineering blog as recently as June 2024 (Building GitHub with Ruby and Rails, https://github.blog/engineering/architecture-optimization/building-github-with-ruby-and-rails/). That post states the application is “nearly two million lines of code,” that “more than 1,000 engineers collaborate on it” daily, and that GitHub deploys “as often as 20 times a day,” with roughly one weekly deploy being a Rails-version upgrade. The single repository
github/githubhouses essentially all of GitHub.com, backed by MySQL and Redis. GitHub has selectively extracted a few services where the workload demanded it — code search (the Blackbird search engine described in 2023, with fundamentally different scaling characteristics than a CRUD app) and an authorization service rewritten in Go outside the monolith — and now describes itself as a hybrid monolith-plus-microservices environment, while continuing to invest heavily in the Rails monolith itself (per the same June 2024 post). The architectural takeaway: even at GitHub’s scale (over a billion API calls per day, hundreds of millions of repositories), the monolith remains the right shape for the core application after more than fifteen years; the extractions are narrow specializations, not a broad migration. -
Basecamp and HEY (DHH’s “Majestic Monolith” Manifesto). David Heinemeier Hansson’s essay The Majestic Monolith, published February 29, 2016 (https://signalvnoise.com/svn3/the-majestic-monolith/), is the most-cited modern defense of the monolith as a deliberate architectural choice. DHH’s own framing is blunt — “Don’t distribute your computing! At least if you can in any way avoid it” — and he grounds it in scale: at the time of writing, Basecamp supported six platforms (web, iOS, Android, Mac, Windows, email) with roughly twelve programmers, a ratio he argues is “simply [not] compatible with a highly labour intensive pattern” like microservices/service-oriented architecture. DHH — co-creator of Ruby on Rails, founder of Basecamp/37signals — argues that the cultural pressure to split everything into microservices has overshot its evidence, and that for small focused teams a monolith is positively better, not just a stepping stone. The argument’s structure: most companies are not Google, Netflix, or Amazon; the operational engineering investment those companies have available to operate microservices fleets is not available to most teams; a single deployable lets a small team move fast, deploy confidently, and avoid the entire taxonomy of distributed-systems failure modes. Basecamp 3 (and its follow-up Basecamp 4) is the canonical “majestic monolith” — a single Rails application owning the entire product, deployed by a team of fewer than fifteen engineers, serving a multi-million customer base reliably for years. DHH’s follow-up product HEY (an email service launched in 2020) was built on the same architecture: one Rails monolith, no microservices, served by a small team. DHH has reiterated the position multiple times since (the 2022 Cloud Exit essay describing 37signals’ move from AWS back to on-prem hardware, and various interviews); his position is that the operational cost of microservices — observability, deployment coordination, distributed-systems failure modes, hiring for the resulting complexity — exceeds the benefits for any team smaller than several hundred engineers. The position is not without critics, but it is the empirically defensible counter-argument to microservices defaultism, and it is grounded in two decades of running Basecamp profitably with this architecture.
The deeper insight in DHH’s writing: the question is not “monolith vs microservices” but “what is the simplest architecture that can deliver the product?” For Basecamp’s product (project-management collaboration), one Rails monolith is the simplest sufficient architecture; adding microservices would be gratuitous complexity. For Netflix’s product (video streaming at planet scale, with regional failover and aggressive A/B testing), microservices are necessary; a monolith would not suffice. The mistake the industry made in 2014–2018 was treating microservices as a default, applicable regardless of team size or product. DHH’s manifesto was the corrective.
-
Etsy (historical). Etsy famously pioneered Continuous Deployment on a PHP monolith in the early 2010s, deploying the same monolith multiple times per day with strong feature-flag discipline (Allspaw et al., various Etsy engineering posts; The DevOps Handbook by Kim, Humble, Debois, Willis covers this case in detail). Etsy’s published deploy cadence at peak was roughly 50 deployments per day, all to the same PHP monolith. The architectural discipline that made this work: extensive feature flags (every new feature shipped dark until it was ready), a comprehensive test suite (the build was kept fast so the deploy pipeline did not bottleneck), and an investment in observability (real-time graphs of every metric). Etsy has since modernized its stack but the historical case shows that “monolith” and “fast deployment cadence” are not in conflict — in fact, the monolith helps with deploy cadence because there is only one artifact to ship. Microservices teams routinely deploy slower than well-disciplined monolith teams because the per-service deploy overhead times the number of services ends up larger than the per-monolith deploy overhead.
-
Slack (historical). Slack famously ran a PHP monolith — affectionately called “the webapp” — well into its growth years. Slack Engineering’s 2018 talk Scaling Slack describes the architecture as a single PHP application backed by MySQL and Memcached, with significant investment in horizontal scaling and database sharding rather than service extraction. Slack has since moved a few specialized concerns out (the real-time messaging gateway, in particular, runs in a separate Java service), but the bulk of the application logic remained in the PHP monolith for years. The lesson: a competently engineered monolith carries a company through hundreds of millions of users.
Uncertain uncertain
Verify: the current (2026) internal architecture of Slack and Basecamp/HEY. Reason: company architectures evolve and are not continuously published. The Slack and Basecamp descriptions above rest on conference talks and essays through ~2022 (Slack’s 2018 Scaling Slack talk; DHH’s 2016 Majestic Monolith and 2022 Cloud Exit). GitHub’s monolithic status, by contrast, is directly confirmed by GitHub’s June 2024 engineering blog and is no longer uncertain. To resolve: find a post-2024 Slack or 37signals engineering write-up on their current deployment shape before quoting specifics in an interview.
7. Tradeoffs
| Dimension | Monolith — pro | Monolith — con |
|---|---|---|
| Deployment | One artifact, one rolling update; trivially reproducible | Cannot deploy a single module independently; release cadence is set by the slowest team |
| Local development | Clone one repo, run one process; debugging is single-process | Build times grow with codebase size; cold-start time can be minutes for very large monoliths |
| Inter-module calls | Nanosecond function calls, no serialization, no partial failure | Tight coupling possible if module boundaries unenforced; refactor risk grows with size |
| Transactional integrity | Cross-module operations are normal database transactions | Cannot mix multiple databases naturally; polyglot persistence is awkward |
| Scaling | Add more identical replicas behind a load balancer | Cannot scale per-module; resource-hungry modules force replicating the whole app |
| Testing | End-to-end tests run in one process; fixtures shared | Test suite grows; selective testing requires module-aware tooling |
| Operational complexity | One service to monitor, alert on, deploy, secure | Single point of failure (mitigated by replicas); blast radius is the whole app |
| Technology heterogeneity | One language, one runtime — simpler hiring, simpler tooling | Cannot adopt a new language for one module without rewriting |
| Team scaling | Works well up to ~50 engineers if module boundaries are clean | Above that, merge conflicts and ownership ambiguity dominate |
| Failure isolation | Process boundary exists at machine level only | A single bad module crashes the whole process; memory leak in feature X kills feature Y |
| Cognitive load | One mental model | The model can become huge; new engineers face a long ramp-up |
| Cost (early stages) | Lowest possible — one deploy target, one DB | n/a |
| Cost (late stages) | n/a | Coordination cost rises superlinearly with team size |
The pattern of the table is the central insight: the monolith’s pros are concentrated at small scale, and the cons emerge at large scale. Choosing an architecture is choosing where you expect to be on this curve.
8. Migration Path
8.1 Into a Monolith
The default arrival: write the first version as a monolith. This is what nearly every successful product looks like in year one. The relevant disciplines on the way in:
- Establish module boundaries early — even before you “need” them. Code organized as
app/orders,app/billing,app/catalogis one short refactor away from a modulith; code organized asapp/controllers,app/models,app/servicesis years away. - Define a
public/andinternal/split per module. Make cross-module imports ofinternal/a build-time error. - Write integration tests at module boundaries. They double as living documentation of what is and isn’t in each module’s contract.
8.2 Out of a Monolith (Strangler Fig)
When a monolith outgrows its team, the canonical migration discipline is the Strangler Fig Pattern (Fowler 2004). Big-bang rewrites of large monoliths fail at high enough rates to be considered a project-management anti-pattern; the strangler-fig method instead extracts services one bounded context at a time, with the monolith and the new service running side-by-side until the monolith’s responsibility is “strangled” away. The mechanical sequence:
- Identify the next bounded context to extract. Use Domain-Driven Design Strategic Patterns to find a coherent area with low coupling to the rest of the monolith. Often the lowest-coupled module is not the highest-value one to extract; pick one where the migration cost is low and that produces a learning the team can apply.
- Establish a façade. Insert an abstraction in the monolith that the rest of the monolith calls, currently routing back to the in-process module. Future calls will be redirected behind this façade.
- Build the new service. Implement the bounded context as a separate deployable, with its own database, talking the protocol of your choice. Do not yet route any traffic.
- Dual-run. Configure the façade to call the new service for a small fraction of requests (canary), comparing results to the in-process module. This catches bugs and performance regressions before they affect users.
- Migrate data. Move the canonical store of the bounded context’s data from the monolith’s database to the new service’s database. Use Change Data Capture (CDC) or batch backfills; maintain dual-writes during the transition.
- Cut over fully. Route 100% of the bounded context’s traffic to the new service.
- Delete the dead code. Remove the old in-process module from the monolith. This is the step where many migrations stall — without it, you’ve added a service and kept the code, which is the worst of all worlds.
Repeat for the next bounded context. Each iteration shrinks the monolith and grows the service mesh. Most migrations stop somewhere short of “no monolith remains” — there is usually a residual core of cross-cutting code that doesn’t fit cleanly in any extracted context. That residual is fine; it is not a failure of the migration, it is the natural endpoint.
8.3 The Reverse Migration (Microservices → Monolith)
A direction the industry rediscovered loudly in the early 2020s. In March 2023 the Amazon Prime Video Video-Quality-Analysis team published Scaling up the Prime Video audio/video monitoring service and reducing costs by 90%, describing how a monitoring pipeline originally built as distributed components orchestrated by AWS Step Functions and AWS Lambda hit a hard scaling ceiling at roughly 5% of the expected load — the dominant costs being per-state-transition Step Functions charges and the constant Amazon Simple Storage Service (S3) round-trips between Lambda stages. The team re-architected the pipeline into a single long-running process on Amazon Elastic Container Service (ECS), keeping tightly coupled components (the media converter and the detectors) together in shared memory to eliminate the inter-stage S3 traffic, and switched from horizontal scaling of many small functions to vertical scaling of one process. The reported result was an infrastructure-cost reduction of over 90% (the post is no longer on the original primevideotech.com URL, which now redirects to a generic Amazon page; it is preserved and quoted in detail by The New Stack’s 2023 coverage, https://thenewstack.io/return-of-the-monolith-amazon-dumps-microservices-for-video-monitoring/). The case is real but narrow: the reverse migration is right when the original split was premature or when the workload turns out to be tightly coupled in ways the microservice boundaries fight against. The mechanical steps are the strangler fig run in reverse — fold each service back into a module of the resurgent process, one at a time.
The interpretation matters as much as the facts, and it is settled by an authoritative primary voice: Adrian Cockcroft — the former AWS VP who led the cloud-architecture team and was one of the public faces of Netflix’s microservices migration — wrote a direct rebuttal to the viral “Amazon abandons microservices” framing (So many bad takes, https://adrianco.medium.com/so-many-bad-takes-what-is-there-to-learn-from-the-prime-video-microservices-to-monolith-story-4bd0970423d4). His position: “This is only one of many microservices that make up the Prime Video application … they called this refactoring a microservice to monolith transition, when it’s clearly a microservice refactoring step.” In Cockcroft’s reading the team did exactly the textbook thing — prototype quickly with serverless, then, once traffic patterns were validated, move the hot, tightly-coupled path onto a continuously-running container — and the result is better described as a “macroservice” (a coarser-grained service) than a monolith. The New Stack reached the same conclusion in a follow-up (Amazon Prime Video’s Microservices Move Doesn’t Lead to a Monolith after All, https://thenewstack.io/amazon-prime-videos-microservices-move-doesnt-lead-to-a-monolith-after-all/). The honest takeaway: the case demonstrates that granularity should fit the workload — over-fine-grained serverless decomposition of a high-coupling, high-throughput data-flow was the original mistake — not that microservices are wrong in general.
The reverse-migration phenomenon has expanded since Prime Video’s case. Notable subsequent examples: 37signals’ 2022 Cloud Exit (DHH and David Heinemeier Hansson’s documentation of moving Basecamp/HEY off AWS managed services and onto owned hardware, with the monolith architecture being a precondition for the move because the operational burden was already minimal); Coinbase’s 2022 internal discussion (extensively reported in industry press) about consolidating some of their microservices fleet to reduce operational toil; the various Datadog and Grafana posts describing pattern-based reverse-migrations among their customer base. The pattern is that organizations whose initial microservices split was driven by cargo cult rather than necessity are the ones most likely to consolidate; organizations whose split was driven by genuine team-scale and workload-isolation requirements are not consolidating because their splits were warranted.
The broader interpretation worth holding: architecture is a response to scale and organizational structure, not a fashion to be followed. Each architecture has a regime of fit, defined by team size, traffic volume, latency requirements, organizational complexity, and operational maturity. A monolith fits a small focused team building a single product; microservices fit a large organization with many product teams whose cadences must be decoupled. Moving between regimes — in either direction — is appropriate when the underlying conditions change. The 2014–2018 mistake was treating the choice as one-way; the 2020s correction is recognizing it as bidirectional.
9. Pitfalls and Anti-Uses
-
Letting the monolith become a Big Ball of Mud. Without enforced module boundaries, internal coupling grows monotonically. Every easy short-term shortcut — “just import this internal helper from the other module” — accumulates into structural rot. Defense: static analysis on imports; module-level code ownership; build-time enforcement. (See Big Ball of Mud Anti-Pattern.)
-
Confusing “monolith” with “no architecture.” A modular monolith is an architecture, with module boundaries, public APIs, and ownership. The architectural choice between monolith and microservices is independent of the choice between disciplined and undisciplined. A disciplined monolith is far better than an undisciplined microservices fleet.
-
Deploying as a coordination event. When every release requires every team’s sign-off, the monolith has become organizational glue. Address either by improving feature-flag discipline (so unfinished work can ship dark) or by extracting the next bounded context.
-
Single database becoming the universal coupling point. Because every module shares a database, the schema becomes a de facto global API surface. A column added for one module shows up in every other module’s queries. Defense: per-module schemas (possibly within the same database server) with module-level access permissions; treat the schema as the module’s encapsulated state, not as a free-for-all.
-
Memory leak in module X crashes module Y. All modules share one process and one heap. A leak anywhere is a leak everywhere. Defense: heap profiling; Out-Of-Memory (OOM) protection in the orchestrator (Kubernetes liveness probes, systemd restart=always); replicas behind the load balancer so a single process restart doesn’t take down the application.
-
Long-running background jobs blocking request handlers. A monolith that mixes synchronous request handling with long-running batch work in the same process can have batch work starve request threads. Defense: separate worker process (still the same monolith binary, but launched in worker mode), separate thread pools, or true offloading to a job queue.
-
Testing the whole thing on every change. A multi-million-line monolith with one CI pipeline has a slow CI pipeline. Defense: incremental build systems (Bazel, Buck, Pants), module-level test selection, parallelization across many CI workers.
-
“Microservices nostalgia” — splitting because the team wants to. Conway’s Law cuts both ways: if engineers culturally believe microservices are the only “real” architecture, they will lobby to split regardless of need. Defense: write the architecture decision down, with evidence (specific scaling numbers, specific team-coordination problems) supporting the choice. (See Architecture Decision Records.)
-
Forgetting the long ramp-up cost. New engineers joining a five-million-line monolith face a multi-month learning curve. The investment is real and should be planned for, not assumed away.
-
Skipping the modulith step on the way out. Trying to go directly from undisciplined monolith to microservices without first establishing internal module boundaries makes the migration vastly harder, because there are no clean seams to cut on. Always traverse the modulith first. The empirical pattern from large migrations (Shopify’s documented work, the various Etsy and Slack writeups, GitHub’s 2014–2020 extractions) is that the modulith pass exposes the actual seams — many of the originally-imagined service boundaries turn out to be wrong, and many extraction “wins” turn out to be unnecessary once the boundaries are clean. Skipping the modulith pass means making boundary decisions blind, then living with them after the migration cost has already been paid.
-
Conflating monolith with single-machine. A monolith is one deployable, not one machine. Production monoliths are deployed across many replicas behind a load balancer; the architectural property is the deployable, not the deployment topology. Engineers who think “monolith means single point of failure” have confused the two; replicas-of-the-monolith is the standard high-availability pattern.
-
Treating database choice as architectural style. “We use Postgres so we’re a monolith” is a confused statement; the database is one decision and the deployable shape is another. A microservices fleet can share a Postgres instance (badly — see Shared Database Anti-Pattern), and a monolith can use multiple databases (a Postgres for transactional state, a Redis for cache, an Elasticsearch for full-text search — this is normal monolith practice). Style is about deployable shape, not data store count.
-
Ignoring the security blast radius. A monolith’s process memory contains every module’s secrets — payment-processor API keys, user PII handlers, internal authentication tokens. A bug in any module that leaks memory or allows code execution exposes all of them. Defense: still run the monolith with least-privilege OS-level credentials, segment secret access per request context (e.g., the catalog handler should never need the payment processor’s API key), and use process-level sandboxing for risky operations (image processing, untrusted user code execution). The microservices architecture’s per-service blast radius is sometimes a real benefit; the monolith mitigates it via discipline rather than process boundary.
10. Comparison With Sibling Architectures
| Property | Monolith | Microservices (Microservices Architecture) | Service-Oriented Architecture (Service-Oriented Architecture) | Modulith (variant) |
|---|---|---|---|---|
| Deployment unit | Single | Per service | Per service (often coarser) | Single |
| Process boundary | One | Many | Many | One |
| Inter-module calls | In-process function call | HTTP / gRPC | SOAP / Enterprise Service Bus | In-process function call |
| Database | One (shared) | Per service (each owns its data) | Often a shared enterprise database | One (typically with per-module schemas) |
| Transactional integrity | Free (DB transactions) | Sagas / 2PC / eventual | Mostly via central database | Free (DB transactions) |
| Independent scaling | No | Yes | Partial | No |
| Independent deployability | No | Yes | Yes | No |
| Operational complexity | Low | High (mesh, observability, deploys × N) | High (Enterprise Service Bus, governance) | Low |
| Team coordination | Single team or coordinated multi-team | One team per service | Centralized governance | Single team or coordinated multi-team |
| Right scale | Small to mid-size org, single product | Large org, many teams, many products | Mid-to-large enterprise, vendor-heavy | Same as monolith — but with cleaner internals |
| Failure isolation | None within process | Per-service blast radius | Per-service blast radius | None within process |
| Polyglot programming | No | Yes | Sometimes | No |
| Migration to next stage | Strangler fig → microservices | (Already at end state, or back to modulith) | Strangler fig → microservices | Strangler fig → microservices, often skipped |
The modulith is essentially “a monolith with the discipline of microservices,” achieving most of microservices’ organizational benefits without the operational costs. The honest summary: most organizations would be better served by a disciplined modulith than by either an undisciplined monolith or a microservices mesh that they aren’t ready to operate.
11. Common Interview Discussion Points
When this architecture is on the table in a system-design interview, expect to be probed on:
- “At what point would you split this monolith?” A strong answer cites organizational triggers (team size, deployment coordination friction, unaligned release cadences) before technical triggers (specific endpoints needing independent scaling, specific subsystems needing different storage). Interviewers want to hear that you do not split prematurely.
- “How would you handle the migration?” The expected answer is the Strangler Fig Pattern with concrete steps: pick a bounded context, build a façade, dual-run, migrate data, cut over, delete the old code. Bonus points for citing Fowler’s original 2004 essay.
- “What’s the difference between a monolith and a modular monolith?” A clean answer distinguishes the deployment shape (same — single deployable) from the internal structure (different — enforced module boundaries vs. a free-for-all).
- “Why would you ever choose a monolith in 2026?” This is a leading question. The defensible answer is Fowler’s MonolithFirst argument: the cost of microservices is paid up front, the benefits accrue over time, and many systems never reach the scale where the benefits exceed the costs.
- “How do you keep a monolith from rotting?” Module boundaries enforced by tooling, ADRs for each significant decision, ownership of each module by a specific team, public/internal API split per module, dependency-direction rules.
- “How would your monolith handle 1M Queries Per Second (QPS)?” Replicas behind a load balancer, read replicas for the database, caching layer (see LRU Cache and Redis), and acceptance that a few specific endpoints might need to be extracted as services even if the rest of the monolith stays. Citing Stack Overflow’s published numbers (billions of monthly page views from a small fleet) is a credibility move.
- “How does a monolith differ from a Service-Oriented Architecture?” SOA also has multiple deployable services, but glued by an Enterprise Service Bus and often a shared enterprise database. Monolith is one-deployable; SOA is multi-deployable. Microservices is SOA pushed further toward per-service ownership and independent deployability.
- “What are the failure modes you’d plan for?” Process crash → multiple replicas, liveness probes, blue-green or rolling deploys. Memory leak → restart-on-OOM. Slow database query → query timeouts and circuit-breaker (see Circuit Breaker Pattern). Bad deploy → canary deployment, fast rollback.
- “What’s the largest monolith you can think of in production?” Stack Overflow, Shopify (Ruby on Rails monolith, modular), Basecamp/HEY, GitHub (until extractions), Etsy (historical PHP monolith with Continuous Deployment).
12. Historical Context and Evolution
The word “monolithic” carried no negative connotation through most of computing’s history; it was simply the default. In the 1960s and 1970s, a “program” was one binary on one mainframe. The IBM System/360 family, the DEC PDP-11 family, and the Unix-on-PDP-11 era all assumed the program was a single executable, with multi-program collaboration achieved through shared files or operating-system primitives (pipes, signals) rather than network calls. The architectural literature did not name this style because it was the only style.
The 1980s and 1990s introduced Client-Server as the first widely-named alternative, with the application split between a thin client (an X11 application, a SQL query tool, a 3270 terminal emulator) and a server holding the data. The “monolithic” label began to acquire meaning by contrast — applications that did not split into client and server. By the late 1990s the 3-tier architecture (browser → application server → database) was the new default, and “monolithic” started to acquire its modern slightly-pejorative connotation: a system that has not yet been tiered.
The 2000s saw two parallel waves. Service-Oriented Architecture (SOA), supported by SOAP, WS-* standards, and Enterprise Service Bus products, fragmented enterprise applications into many cooperating services with a centralized integration layer; this was the precursor to microservices. Simultaneously, web companies (Google, Amazon, eBay) at scales above what enterprise SOA had targeted independently developed similar fragmentations driven by team-scaling rather than vendor sales motions. The earliest published case of large-scale service splitting is Werner Vogels’s 2006 ACM Queue interview A Conversation with Werner Vogels, where he described how Amazon had decomposed its retail application into hundreds of services, each with its own data store.
The 2014 publication of Fowler and Lewis’s Microservices essay (https://martinfowler.com/articles/microservices.html) gave the modern movement its name. The years 2014–2018 saw aggressive industry adoption — and aggressive premature adoption. Many organizations split monoliths they did not yet need to split, and discovered they had built distributed monoliths with all the costs of microservices and none of the benefits. This experience produced the 2015 corrective from Fowler — the MonolithFirst essay — and Sam Newman’s 2021 second-edition Building Microservices explicitly opens with the recommendation to start monolithic.
The 2020s have seen a measured re-evaluation. Amazon Prime Video’s 2023 case (the audio/video monitoring service consolidating from Step Functions + Lambda back into a single process) was the most-discussed instance, but the broader industry has moved toward a more nuanced view: the right architecture depends on team scale, organizational maturity, operational sophistication, and the nature of the workload. The modular monolith — Shopify’s published architecture, Su & Verburg’s 2024 industry summary — is the synthesis: monolithic deployment with disciplined internal modularity. This is, plausibly, the dominant architecture for the next decade of greenfield application development.
Several other 2020s data points reinforce the re-evaluation. Stack Overflow’s 2024 architecture overview confirms the same monolith continues to serve their traffic with minor evolution. DHH’s 2022 Cloud Exit essay describing 37signals’ move from AWS back to on-premise hardware was framed around the operational simplicity their monolith makes possible (running a monolith on owned hardware is straightforward; running a microservices fleet on owned hardware is a substantial undertaking). Frank Lyaruu’s Modular Monolith talks at NDC and similar conferences in 2023–2024 articulate the modulith doctrine for .NET audiences. Spring’s official endorsement of the modulith pattern via Spring Modulith (Drotbohm 2022+, https://docs.spring.io/spring-modulith/reference/) gave the doctrine official framework support in the Java ecosystem. The 2024 SQL Server team at Microsoft published a series on running large workloads on single-instance Postgres / SQL Server with proper indexing and partitioning, showing that the database scaling fears that drove much microservices migration were often unwarranted at the actual workload sizes in question.
The intellectual history is worth noting because it has been compressed and sometimes rewritten in subsequent retellings. The microservices movement did not begin with Fowler and Lewis’s 2014 essay; the patterns were already in production at Amazon by 2002 (the Bezos API mandate, where Jeff Bezos famously decreed that all teams must expose data and functionality through service interfaces and that all such interfaces must be designed to be externalizable as third-party APIs), at Netflix from approximately 2008 onward (Netflix’s well-documented decade-long migration from a Oracle-backed monolith to AWS-hosted microservices, driven by their 2008 catastrophic database corruption incident), and at Google internally from roughly the same era. The 2014 Fowler/Lewis essay named the pattern; the pattern itself had been emerging for over a decade. The cultural mistake of 2014–2018 was not that microservices were invented but that they were generalized — pushed onto teams whose scale, problem domain, and organizational maturity did not warrant them. The 2020s correction is the recognition that the appropriate question is not “monolith or microservices?” but “what scale of deployment unit fits this team and this workload?”
The Bezos API mandate (informally documented in a 2002 internal memo, later quoted by Steve Yegge in his 2011 Stevey’s Google Platforms Rant — https://gist.github.com/chitchcock/1281611) is worth understanding as a historical artifact. The mandate’s six points: (1) all teams will henceforth expose their data and functionality through service interfaces; (2) teams must communicate with each other through these interfaces; (3) there will be no other form of inter-process communication allowed (no direct linking, no direct reads of another team’s data store, no shared-memory model, no back-doors whatsoever); (4) the technology choice doesn’t matter (HTTP, CORBA, Pub/Sub, custom protocols — doesn’t matter); (5) all service interfaces must be designed from the ground up to be externalizable, that is, the team must plan and design to be able to expose the interface to developers in the outside world without exception; (6) anyone who doesn’t do this will be fired. The mandate is the moment “service-oriented architecture” became internal Amazon practice; everything Amazon did subsequently grew from this. But — crucially — Amazon was a several-thousand-engineer organization at the time, and the mandate was a response to Amazon’s coordination cost, not a generic best practice. The 2014–2018 microservices movement universalized the mandate without preserving its context.
13. Quantitative Capacity of a Modern Monolith
A frequently-asked interview probe is “how far can a monolith scale?” The honest engineering answer requires order-of-magnitude estimates. Consider a single modern bare-metal server (circa 2026): two-socket Intel Xeon or AMD EPYC, ~64 cores, 256–512 GB RAM, NVMe SSDs, 25 Gbps NIC. Such a machine running a competently-written application server can handle, very roughly:
- HTTP request throughput: 50,000–200,000 requests per second for moderately complex requests (a few-millisecond CPU per request, a few hundred microseconds of database time). Stack Overflow’s published numbers (Craver 2016) put their peak at roughly half this band, with significant headroom.
- Database throughput on the same machine: modern Postgres on NVMe can sustain tens of thousands of transactions per second on a single primary; reads scale further with replicas. Amazon Aurora claims ~500K reads/sec and ~200K writes/sec per cluster, with the cluster horizontally bounded but the API monolithic to clients.
- Concurrent connections: with epoll/kqueue/IOCP-based async runtimes (Node.js, Tokio, Netty, Vert.x), a single process can maintain hundreds of thousands of idle TCP connections. The C10K problem of the early 2000s is, by 2026, the C10M problem and the answer is “yes, with the right runtime.”
- Storage: terabytes-to-low-tens-of-terabytes per local NVMe; multi-petabyte for cluster filesystems. An application generating 100 GB of state per day fits comfortably for years.
- Memory bandwidth: modern DDR5 systems deliver ~50–100 GB/s per socket; in-process data manipulation that fits in RAM is dramatically faster than equivalent operations crossing process boundaries.
- Working set: a 256 GB RAM machine comfortably holds the working set of any application whose hot data is below ~200 GB. Most line-of-business applications have hot working sets in the few-GB-to-low-tens-of-GB range.
The numbers compound. A modern bare-metal server can serve 100,000 requests/second with sub-millisecond P50 latency, hold 200 GB of hot data in memory, sustain hundreds of thousands of concurrent connections, and persist tens of terabytes of state — all in one process. The headline scale numbers that cause engineers to assume “we need microservices” are very often within this single-machine envelope. The 2026 discussion of “what scale needs distribution” is dramatically different from the 2010 discussion; hardware has improved so much that the scale ceiling for a monolith has risen to a point above what most product companies will ever reach.
A specific datapoint: Discord’s 2017 engineering blog post How Discord Stores Billions of Messages describes their migration from MongoDB to Cassandra — a database-tier scaling decision driven by storage volume, not by application-tier scaling. The application tier itself remained a relatively small Elixir/Erlang fleet. The lesson: scaling questions are often database questions, not application-architecture questions; replacing the database is usually less disruptive than splitting the application.
These numbers say something important: most companies’ actual workloads, even at scale-up rounds, fit on one (well-replicated) machine. The “scale to a billion users” use case is rare; the “scale to a million daily active users” use case is common, and a competent monolith handles it without strain. The architecture decision should be calibrated to the actual workload, not to the workload one aspires to have.
Uncertain uncertain
Verify: the specific requests-per-second / transactions-per-second / connection-count figures in this section. Reason: they are deliberately order-of-magnitude estimates synthesized from heterogeneous published benchmarks (the Stack Overflow architecture posts give one concrete real-world anchor — ~209M HTTP requests/day served with ~9-fold headroom on the web tier — but Aurora throughput claims, async-runtime connection counts, and DDR5 bandwidth figures come from vendor docs and benchmarks under conditions unlike any specific application). Actual numbers depend strongly on workload shape, language/runtime, and tuning, and can differ by 10× either way. To resolve: benchmark your own workload before committing to a capacity plan; treat these only as “is distribution even necessary?” sanity checks, not as a design spec.
14. Case Studies in Depth
14.1 Stack Overflow’s Monolithic Survival
Nick Craver’s 2016 architecture overview describes Stack Overflow’s deployment in detail. The application is a single ASP.NET / C# monolith running on the Microsoft .NET Framework. The deployment in 2016 consisted of 11 IIS web servers (9 primary plus 2 staging — and Craver reports the load could be served by only 1 web server in a pinch, the rest being headroom), 4 SQL Server database servers (two clusters with replicas), 2 Redis servers, 3 tag-engine servers, 3 Elasticsearch servers, and 4 HAProxy load balancers. Stack Overflow’s reported load was on the order of 209 million HTTP requests and 66 million page loads per day across the entire Stack Exchange network (figures for February 9, 2016) — and they were comfortably within capacity at this scale.
The key architectural disciplines that made this possible: aggressive caching (Redis as a hot-data tier in front of SQL Server, with carefully chosen cache invalidation strategies); read-replica scaling for the database; deliberate avoidance of premature splits (when a feature was added, it was added to the monolith, not as a new service); and ruthless performance discipline (every page load was budgeted in milliseconds, with profiling tools surfacing regressions immediately).
A specific architectural detail Craver documents that deserves emphasis: Stack Overflow uses raw SQL (via the Dapper micro-ORM, which Stack Overflow itself open-sourced in 2011) rather than an Object-Relational Mapper that hides the SQL behind objects. The reasoning: at Stack Overflow’s traffic volume, the difference between a hand-tuned query and an ORM-generated query is significant; the operational discipline is to write the SQL deliberately rather than let the ORM emit it. This is not a universal recommendation — most applications are not at Stack Overflow’s scale and benefit from ORM productivity — but it illustrates that monoliths can apply per-query optimization in ways microservices fleets cannot easily replicate (because the equivalent in microservices is per-service per-query optimization across many services, with cross-service consistency now an additional concern).
Another Craver-documented discipline: the deploy process. Stack Overflow’s deploy is fast — minutes from commit to production — because the monolith is one artifact, deployed via rolling restart across the nine-server fleet. Each server is taken out of rotation, the new build is installed, the server is warmed up by a synthetic-traffic generator, the server is added back to rotation. This rolls through the fleet in under ten minutes for a typical deploy. The equivalent microservices deploy at Stack Overflow’s traffic level would have to coordinate across many services, each with its own deploy pipeline and warm-up time; the operational complexity would be substantially greater for the same end-user impact.
The implication for system-design interviews: do not assume “high scale” requires microservices. Stack Overflow’s traffic exceeds what most system-design candidates discuss for hypothetical systems, and the production architecture is a small monolith fleet. Quoting this case in an interview demonstrates that the candidate has actually thought about real-world architectures rather than reciting microservices dogma. The case also illustrates the defensive value of the monolith: when something breaks at Stack Overflow, it is one process to debug; the alert page, the metric dashboard, and the deploy log all point at one artifact. The microservices equivalent would be a many-pane dashboard with cross-service correlation as a continuous engineering investment.
14.2 Shopify’s Modular Monolith
Shopify Engineering’s 2019 essay Deconstructing the Monolith describes the transformation of a large Ruby on Rails monolith from “undisciplined” to “modular.” Shopify built internal tooling (open-sourced as packwerk) that statically analyzes Ruby imports and enforces module-level visibility rules: each module declares its public API, and packwerk rejects code that imports another module’s private internals.
The migration path: identify natural module boundaries based on bounded contexts (Domain-Driven Design strategic patterns, Vernon 2013); incrementally annotate the codebase with module boundaries; turn the static-analyzer warnings into errors as each module’s boundary becomes clean. The effort took years. The reported outcome: Shopify retained the operational simplicity of a single Ruby on Rails deployment (still serving a non-trivial fraction of global e-commerce) while gaining most of the developer-productivity benefits attributed to microservices — clear ownership, replaceable internals, fast onboarding within a module.
Shopify’s architecture is the published case for the “modular monolith first” recommendation. Engineers considering microservices migration should read this essay before committing to the split.
14.3 Amazon Prime Video’s Reverse Migration
Amazon Prime Video’s March 2023 engineering write-up Scaling up the Prime Video audio/video monitoring service and reducing costs by 90% describes consolidating a Step Functions-orchestrated set of AWS Lambda functions into a single long-running process on Amazon Elastic Container Service (ECS). The original architecture orchestrated many small Lambda invocations through Step Functions, paying for every state transition and shuttling intermediate data through Amazon S3 between stages; it hit a hard scaling limit at roughly 5% of the expected load. The consolidated architecture keeps the media converter and detectors together in one process with shared memory, eliminating the inter-stage S3 traffic, and scales vertically. Reported metric: over 90% infrastructure-cost reduction for this specific workload (frame-level video analysis with high inter-frame coupling).
The honest interpretation is not “Amazon abandoned microservices,” and the best authority on this is unambiguous. Adrian Cockcroft — former AWS VP and a public architect of Netflix’s microservices era — wrote that this was “clearly a microservice refactoring step,” “only one of many microservices that make up the Prime Video application,” better described as a move to a coarser-grained macroservice than to a monolith (https://adrianco.medium.com/so-many-bad-takes-what-is-there-to-learn-from-the-prime-video-microservices-to-monolith-story-4bd0970423d4). It is one workload at one team, where over-fine-grained serverless decomposition was a poor fit for a high-coupling, high-throughput data-flow. The general lesson: architecture granularity should fit the workload, and the choice cuts both ways — premature splits should be reversed as readily as premature consolidations should be split. (The original primevideotech.com post is no longer at its URL; it is preserved in The New Stack’s contemporaneous coverage, https://thenewstack.io/return-of-the-monolith-amazon-dumps-microservices-for-video-monitoring/.)
15. The “Strangler Fig” Origin and Its Importance
Martin Fowler coined the term Strangler Fig Application in a 2004 essay (https://martinfowler.com/bliki/StranglerFigApplication.html), drawing the metaphor from the Ficus tree species. A strangler fig grows on a host tree, gradually wrapping it; eventually the original host dies and decomposes, leaving the fig standing in its place. Fowler’s insight: this is the right metaphor for migrating a monolith. You do not chop the monolith down (a big-bang rewrite); you grow new services around it, slowly redirecting traffic, until eventually no traffic flows to the original code and it can be deleted.
The pattern’s importance to monolithic-architecture discussions is that it provides the answer to the inevitable migration question. When an interviewer asks “how would you migrate this monolith to microservices?”, the wrong answer is “we’d plan a six-month rewrite project”; the right answer is “we’d identify the next bounded context, build a strangler-fig facade, dual-run, migrate data, cut over, and delete the old code, repeating for each bounded context until the monolith is the right size — possibly never reaching zero.”
The empirical record on big-bang rewrites is brutal. Joel Spolsky’s 2000 essay Things You Should Never Do, Part I (https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/) makes the case at length, citing the Netscape rewrite that destroyed Netscape’s market position. Successful migrations of significant monoliths almost universally use the strangler-fig pattern; failed migrations almost universally attempted a big-bang rewrite. Engineers who read this history before committing to a rewrite save themselves enormous pain.
15.5 What “Monolith” Means Today — A Disambiguation
The word “monolith” has accumulated multiple meanings in industry usage, and conflating them is the source of much architectural confusion. The serious modern case for the monolith hinges on which meaning is intended; defenders and critics often talk past each other because they are imagining different things. The taxonomy worth holding in mind:
Meaning 1: Single-deployable, modular monolith. One artifact, one process, one database, internal modules with enforced boundaries, clear per-team ownership. This is the architecture Shopify, Basecamp, and Stack Overflow defend. It is the architecture Fowler advocates in MonolithFirst. It is structurally sound, operationally simple, and scales to the team and traffic levels of most successful companies. When DHH says “majestic monolith,” he means this. When Westeinde describes Shopify’s architecture in 2019, she means this. The single-deployable-modular monolith is the strong form of the monolith.
Meaning 2: Single-deployable, undisciplined monolith (“Big Ball of Mud”). One artifact, one process, no internal structure, modules that import each other’s internals freely, the database schema as a free-for-all global namespace. This is what most engineers actually built in the 2000s before the architectural literature pushed back. It is what Foote and Yoder named in 1997. It is the weak form of the monolith — the failure mode whose pain motivated the microservices movement. The serious modern case for the monolith is not a defense of this form; both Fowler and DHH explicitly distinguish the two.
Meaning 3: A dismissive label for “any application that hasn’t been microservice-ified yet.” This is the meaning the most aggressive microservices proponents implicitly use: “your monolith” as a placeholder for “your unmodernized legacy code, which we should be migrating to services.” Conflating Meaning 1 and Meaning 3 is what produces the false dichotomy “monolith vs microservices” — the actual choice is between three architectures (Meaning 1 modulith, Meaning 2 ball-of-mud, full microservices), and the right answer for most organizations is Meaning 1.
Meaning 4: Pre-distributed-systems architecture, period. Some critics use “monolith” to mean any single-machine application, with the implicit claim that distributed systems are inherently superior. This is not a defensible position — single-machine applications are dramatically simpler and faster than distributed equivalents whenever they fit the workload, and most modern monoliths are replicated across many machines (one deployable, many replicas) rather than literally single-machine.
The distinction between Meaning 1 and Meaning 2 is the crux of whether “monolithic architecture” is good or bad in any specific context. The modular monolith is the modern serious case; the undisciplined monolith is what every architecture style — including microservices — degrades into without explicit discipline. A microservices architecture without enforced contracts becomes a Distributed Monolith Anti-Pattern (the worst of both worlds); a monolith without enforced module boundaries becomes a Big Ball of Mud. The disciplined-vs-undisciplined axis is orthogonal to the deployment-shape axis.
15.6 Migration Triggers — Concrete Metrics
A perennial system-design question is “at what point should we extract from a monolith?” The defensible answer is metric-driven, not folklore-driven. Specific triggers, with quantitative thresholds where they exist:
Organizational triggers (usually fire first):
- Team count exceeds ~5 squads on the same codebase. Below this, coordination is conversational; above it, coordination requires process, and the monolith becomes the bottleneck of inter-team negotiation. Westeinde 2019 puts Shopify’s modulith inflection point in this range; Drotbohm’s Spring Modulith advice cites similar numbers.
- Headcount exceeds ~50 engineers contributing to the codebase. Above this, code review becomes a queue, build times become noticeable across the team, and the merge-conflict rate grows superlinearly. Newman 2021 (chapter 1) suggests the same threshold from independent observation. Below 50, the operational cost of microservices exceeds the gains.
- Deployment frequency drops below desired cadence due to coordination cost. If teams routinely defer deployments to align with each other, or if a single bad change blocks ten teams’ ships, the monolith has become coordination glue. Etsy ran 50 deploys/day on a monolith — the upper bound is high if discipline is high; if your team is below 5 deploys/week and wants 5 deploys/day, fix discipline first, extract second.
- Different teams want fundamentally different release cadences. When one team’s product needs daily releases and another’s requires monthly compliance review, the same monolith cannot serve both efficiently. This is a strong extraction signal.
Technical triggers (fire second, usually in support of organizational triggers):
- Codebase exceeds ~1M lines of code with growing build/test times. The exact threshold depends on language (Ruby and Python suffer earlier than Java or Go due to test-suite speed); the symptom is build times exceeding 10–15 minutes consistently and integration test suites taking hours. Caching and parallelization buy time but not infinite time.
- One specific module needs polyglot persistence or polyglot programming. If 95% of the application is fine in Postgres + Ruby but one module genuinely needs a graph database or a Python ML library, extraction is justified; the monolith cannot absorb the heterogeneity without losing its operational simplicity.
- One specific module’s resource profile diverges sharply from the rest. A module that needs 100GB RAM, GPU acceleration, or a special-purpose runtime is a candidate for extraction. The whole-monolith replication forces every replica to provision for the worst-case module.
- Throughput requirements exceed single-database-cluster capacity for a specific bounded context. If
orderswrites are saturating the shared Postgres primary whilecatalogreads are barely loaded, extractingordersto its own datastore makes sense; the cost is database-coupling-removal, not simple extraction. - Failure isolation becomes a regulatory or business requirement. A regulatory regime that requires the payment subsystem to be auditably isolated from the rest of the application; a business commitment to keep the search engine running even if the storefront is down. These are cell and bulkhead arguments (Bulkhead Pattern, Cell-Based Architecture) and they justify per-context extraction.
The honest summary: organizational triggers fire first, almost always. Technical triggers are usually invoked after organizational pressure has mounted, as the post-hoc justification. An interview answer that leads with “we’d extract when team coordination cost exceeds the operational cost of the new service” is more defensible than one that leads with “we’d extract when the database hits 1k QPS.”
15.7 The “Rebuild Instead of Split” Alternative
A distinct option, often overlooked in the monolith-extraction discussion, is to rebuild the monolith from scratch as a new monolith rather than extract pieces of the old one. This is the Netscape rewrite pattern, named after the famous 2000 case of Netscape rewriting their browser engine and losing market position to Internet Explorer in the process — the lesson being that rewrites take longer than expected and the new system has to catch up to the old system’s accumulated bug fixes and feature parity. Joel Spolsky’s 2000 essay Things You Should Never Do, Part I (https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/) makes the cautionary case at length.
But the rebuild-from-scratch option is not always the wrong choice. When a monolith is genuinely past saving — the schema has accumulated decades of legacy decisions, the code is in a language no one wants to maintain (classic ASP, COBOL), the framework has been abandoned by its maintainers — extracting one bounded context at a time may take longer and produce a worse result than declaring the old system frozen, building a new monolith fresh, and migrating users in waves. The architectural decision is: do we strangler-fig (extract context-by-context to a service mesh), do we modulith-first (clean up the old monolith’s internals), or do we rebuild (start over with a clean monolith)?
The defensible criteria for rebuild over strangler:
- The old monolith’s data model is so contrived by accumulated decisions that any extraction inherits the contortions. The new monolith starts from a clean schema.
- The old monolith’s framework or language is no longer maintained, and migrating piece by piece would mean keeping the dead framework alive longer than necessary.
- The new product requirements have diverged enough that the old monolith’s assumptions are wrong everywhere; piecewise extraction would carry forward the wrong assumptions.
- The team is small enough and the product simple enough that a clean rewrite is achievable in a year or less. (For large products, rewrites take many years and usually fail.)
The defensible criteria for strangler-fig (extraction) over rebuild:
- The old monolith’s data is the asset; the code is replaceable but the data shape and history are valuable. Extraction lets the data stay in place while the code around it changes.
- The old monolith is generating revenue and cannot be paused; the new system must coexist with the old indefinitely. Strangler-fig preserves continuity.
- The team is large enough that a parallel “build the new system” track and “operate the old system” track can run simultaneously without starving each other.
- The product is too complex for a clean rewrite; the accumulated business rules in the old system are themselves the spec.
In practice, modulith-first is the most under-used option. Many teams that conclude they need to rebuild or strangler-fig actually have a monolith that would respond well to internal modularization. The modulith path is cheaper, lower-risk, and often produces 80% of the benefit at 20% of the cost. Engineers are biased toward dramatic structural changes (rebuild! microservices!) because they are visible and signal effort; cleaning up the existing monolith is less glamorous but often the right call.
15.8 Performance Characteristics — Why Monoliths Win on Latency
A frequently-underweighted property of monoliths is that in-process function calls are roughly four orders of magnitude faster than network calls. This is a hardware fact, not an opinion, and it has profound architectural consequences. Specific numbers (cited from Latency Numbers Every Programmer Should Know, Norvig / Dean):
- L1 cache reference: ~0.5 nanoseconds.
- L2 cache reference: ~7 nanoseconds.
- Branch mispredict: ~5 nanoseconds.
- Mutex lock/unlock: ~25 nanoseconds.
- Main memory reference: ~100 nanoseconds.
- Function call overhead in a managed runtime: ~50–200 nanoseconds.
- Send 1KB packet over loopback (in-machine network): ~10 microseconds.
- gRPC call within a data center: ~500 microseconds to 5 milliseconds (depending on payload size, serialization, and TLS).
- Cross-AZ HTTPS call: 1–10 milliseconds.
- Cross-region call: 30–300 milliseconds depending on geography.
The function call is approximately 10,000× faster than the in-data-center gRPC call. A request that traverses ten internal-module boundaries inside a monolith pays roughly 1 microsecond of inter-module overhead; the equivalent path in a microservices architecture pays roughly 5–50 milliseconds. For latency-sensitive paths — interactive web requests where every millisecond is visible to the user — this matters enormously.
The microservices architecture tries to claw back this latency through aggressive parallelization (call ten services in parallel rather than sequentially), edge caching (Content Delivery Network System Design), read-side denormalization (precompute the answer so the query is cheap), and connection-pooling tricks (HTTP/2 multiplexing, gRPC streaming). These help but do not eliminate the fundamental tax. The monolith simply does not pay the tax in the first place; for any path where in-process composition is sufficient, the latency floor is the database query, not the inter-module overhead.
This explains why monolith defenders are not fighting last decade’s war. The latency advantage is increasing in importance as user expectations rise: every millisecond of additional latency reduces engagement metrics (Amazon’s famous early-2000s experiments showed 100 ms of added latency reduced conversion by 1%; Google’s experiments showed similar effects on search engagement). Modern web teams spend significant engineering investment on latency reduction; doing so in a monolith is straightforward (profile, optimize hot paths, cache); doing so in a microservices fleet is a continuous architectural challenge.
The implication for system-design interviews: when latency matters and the workload fits in a single deployable, the monolith is positively faster, not just simpler. This is a defensible technical argument, not just an organizational one.
16. The “Modulith” Specifically
Before closing, a note about the specific term modulith (a portmanteau of “modular” and “monolith”), now used widely enough to deserve treatment as a recognized variant. The term was popularized roughly 2018–2022 by industry writers and conference speakers — Spring Framework’s Oliver Drotbohm contributed materials documenting the discipline at the framework level, and Su & Verburg’s 2024 industry summary consolidated the practice.
A modulith is a single deployable that, internally, has the discipline of a microservices architecture: bounded contexts as modules, public APIs per module, build-enforced encapsulation, in-process events between modules (often via the Outbox Pattern), and per-module ownership. The key distinction from “a monolith with packages” is that the boundaries are enforced, not merely conventional — Shopify’s packwerk tool is the canonical implementation of this enforcement.
The modulith’s pragmatic appeal is that it is easy to extract from if a future split becomes warranted. Each module already has a public API; replacing the in-process call with an HTTP/gRPC call is a known-bounded change. The modules’ data ownership is already enforced (each module owns its tables); replacing in-process database calls with an API to the now-extracted service is again a known-bounded change. This is why Fowler’s MonolithFirst recommends starting with a modulith: it preserves optionality.
Concrete modulith tooling by ecosystem (as of 2024–2026):
- Ruby (Rails). Shopify’s
packwerk(https://github.com/Shopify/packwerk) is the canonical static-analyzer for module boundaries; Gusto’sbundlerextensions add complementary enforcement. Both fail builds on cross-boundary violations. - Java/Kotlin (Spring). Spring Modulith (Drotbohm 2022+, https://docs.spring.io/spring-modulith/reference/) provides annotations (
@ApplicationModule), runtime checks, and integration tests that simulate inter-module API calls. ArchUnit (https://www.archunit.org/) offers more general dependency-rule testing for any Java codebase. - TypeScript/JavaScript. Nx monorepo tooling (https://nx.dev/) provides module-boundary enforcement via
enforce-module-boundariesESLint rules. dependency-cruiser (https://github.com/sverweij/dependency-cruiser) offers similar enforcement for any JavaScript project. - C#/.NET. NetArchTest (https://github.com/BenMorris/NetArchTest) provides ArchUnit-style architecture testing for .NET. The MediatR + module-per-feature pattern is a common idiomatic approach in modern ASP.NET.
- Python. import-linter (https://import-linter.readthedocs.io/) provides import contracts and forbidden-import enforcement for Python projects. The pattern is less mature in Python than in Ruby or Java but increasingly common.
- Go. Go’s package-level visibility (lowercase = unexported) provides a primitive form of module-boundary enforcement; tools like
gomodulesand architecture-test libraries extend this.
The deeper engineering observation is that module-boundary enforcement is a build-time concern, not a runtime concern. A modulith that relies on developer discipline alone reverts to a Big Ball of Mud within months because new engineers don’t know which boundaries exist. A modulith with build-enforced boundaries — where a forbidden import causes a compile error or build failure — remains a modulith indefinitely because the boundaries are mechanical rather than cultural. This is the load-bearing engineering investment that separates a working modulith from a “we have packages but they’re not really enforced” pseudo-modulith. Without enforcement, the modulith is just a hoped-for ideal; with enforcement, it is a structural property of the codebase.
A second engineering observation: the granularity of modules in a successful modulith tracks the team boundaries (Conway’s Law applied positively), not arbitrary technical layers. A cart module owned by the cart team is a sustainable boundary; an entities module owned by no one is not, because no one has the authority to evolve its public API. Modulith design starts with team-and-product boundary mapping, not with package organization. Westeinde 2019 emphasizes this point: Shopify’s modulith boundaries were drawn around product-team responsibilities, with the technical implementation flowing from the product organization.
17. See Also
- System Architectures MOC — parent
- SWE Interview Preparation MOC — grandparent
- Microservices Architecture — the post-monolith style
- Service-Oriented Architecture — the precursor
- Distributed Monolith Anti-Pattern — the failure mode of premature splitting
- Big Ball of Mud Anti-Pattern — the failure mode of an undisciplined monolith
- Strangler Fig Pattern — the canonical migration discipline
- Layered Architecture (N-Tier) — a common internal structure of a monolith
- Hexagonal Architecture — a less common but powerful internal structure
- Conway’s Law — why monoliths and team boundaries get out of sync over time
- Domain-Driven Design Strategic Patterns — bounded contexts as candidate split lines
- Outbox Pattern — for reliable event emission from a modulith
- Shared Database Anti-Pattern — what happens if you split only the code and not the data
- Architecture Decision Records — for documenting why you chose monolithic
- Saga Pattern — what you have to do instead of database transactions once you split
- Two-Phase Commit — the heavyweight alternative to sagas in distributed transactions
- API Gateway System Design — common front for both monoliths and microservices
- Service Mesh System Design — the infrastructure layer needed only once you’ve split