0% completed
Monolith Architecture
On This Page
Know the Vocabulary
Part 1: What Is a Monolith
The Definition
What a Monolith Is Not
Part 2: The Internal Structure of a Well-Designed Monolith
The Layered Structure
The Modular Monolith
Domain-Driven Design in a Monolith
Part 3: The Request Lifecycle in a Monolith
Handling an HTTP Request
The Transaction Advantage
Part 4: Deployment in a Monolith
The Build and Deploy Pipeline
Rolling and Blue-Green Deployment
Database Migrations in a Monolith
Part 5: The Real Advantages of Monolithic Architecture
Development Velocity at Small Team Size
Simplicity of Operational Model
Consistency Through Shared Transactions
Performance of In-Process Communication
Easier Refactoring and Feature Evolution
Part 6: What Eventually Breaks in a Monolith
Deployment Coupling
Resource Scaling Inefficiency
Technology Lock-In
Cognitive Overhead at Large Codebase Size
The Monolith Against the Five Quality Attributes
Part 7: When to Stay Monolithic and When to Extract Services
Stay Monolithic When
Extract a Service When
The Extraction Strategy
FAQs
Key Takeaways
Know the Vocabulary
This lesson uses the terms below repeatedly. Read them once now and return here when a later section needs one.
- Deployable artifact: One packaged build of an application, such as a JAR file or a Docker image, shipped to servers as a unit.
- Process: One running instance of a program, with its own memory, that the operating system starts and stops as a whole.
- Module: A group of related code, such as everything about orders, with a defined interface that other code calls.
- Modular monolith: One deployable application whose code is split into modules by business capability, each with its own interface and its own data.
- Big ball of mud: A codebase with no clear structure, where any code can access any other code's internal details.
- Load balancer: A server that receives incoming requests and spreads them across several copies of an application.
- Horizontal scaling: Adding more copies of an application on more machines; vertical scaling instead gives one machine more CPU or memory.
- Layer: A horizontal slice of the code with one technical job, such as handling HTTP or talking to the database.
- Repository: The class that reads and writes one kind of database record on behalf of the business logic.
- Coupling: The degree to which a change in one part of a system forces a change in another part.
- Bounded context: In domain-driven design (DDD), the part of a system inside which one model and its vocabulary stay consistent, such as inventory or billing.
- Domain event: A record that something happened in the business, such as OrderCreated, that other modules listen for.
- Transaction: A group of database writes that either all succeed or all roll back together (atomicity).
- Saga pattern: A sequence of local transactions in separate services, where each step has a compensating action that undoes it on failure.
- Eventual consistency: A state where parts of a system hold different data for a short time and agree later.
- Feature flag: A runtime switch that turns a code path on or off for some users without a new deployment.
- Strangler fig pattern: A migration approach where a new service takes over one slice of a monolith at a time until the old code is deleted.
Part 1: What Is a Monolith
The food-delivery team from the What Software Architecture Is lesson ships its backend twice a week. Every release contains changes from all three teams, because all six modules are built into one artifact and deployed together. That single decision explains most of this lesson: why the backend is fast and simple to run, and why the Fulfillment team now waits for releases it does not need. This lesson asks when that trade is worth making. Its code uses an online store, and its later parts return to the food-delivery backend where the comparison helps.
The Definition
A monolithic application is one where all of the application's functionality is packaged into a single deployable unit, and each running copy of that unit holds all of the functionality in one process. The user interface, business logic, data access layer, and all integrations exist in one codebase, are built together, tested together, and deployed together.
The key properties that define a monolith:
-
Single deployable artifact: The entire application is packaged into one deployable unit: a JAR file, a compiled binary, or a Docker image containing the full application. Deploying a change to any part of the application means deploying this entire artifact.
-
Shared process space: Each running copy holds every component in one process, so a call between components never leaves that process. A deployment often runs several such processes from the same artifact, such as web processes and background job workers, sometimes on separate machines.
-
In-process communication: Components communicate with each other through direct function calls in memory, not through network calls. A function in the user module calling a function in the payment module is a nanosecond operation with no serialization and no network round trip that can time out.
-
Usually one shared database: All components usually share one database. The user module and the order module query the same PostgreSQL instance. A modular monolith, described later in this lesson, still divides ownership inside that database: each module owns its own schema or tables.
The fundamentals chapter named these four properties as the decisions this style fixes. The first is the one that makes a system a monolith. The other three nearly always come with it, and a monolith that also uses a cache or a search index is still a monolith.
What a Monolith Is Not
A monolith is not necessarily a big ball of mud. A big ball of mud, as defined in the vocabulary above, is a failure of design discipline, not a property of monolithic architecture. The Coupling, Cohesion, and Dependencies lesson shows how it forms, one direct access to another part's internals at a time.
A well-designed monolith has clear internal structure. Domain boundaries are enforced even without deployment separation, and modules communicate through defined interfaces. The business logic layer does not bypass the data access layer, and the presentation layer does not contain business rules. The network does not enforce these boundaries, but code organization, automated checks and team discipline can.
A monolith is also not necessarily a single server. Many production monoliths run multiple instances of the same application behind a load balancer. The application is a monolith (one codebase, one deployable artifact) and it runs horizontally scaled across many servers.
Part 2: The Internal Structure of a Well-Designed Monolith
The Layered Structure
A well-designed monolith organizes its code into horizontal layers, each with one technical responsibility. The classic three are the presentation layer, the business logic layer and the data access layer, and many applications add a fourth, the integration layer. Controllers parse requests, services hold the business rules, repositories talk to the database, and the integration layer wraps external systems such as payment processors and email providers. The Layered (N-Layer) Architecture lesson teaches each of these layers in depth, including the integration layer in its four-layer variant.
The diagram below shows the four layers, and the code after it follows one request through the first three. The controller only authenticates and parses the request and formats the response, the service holds every business rule, and the repository only talks to the database. The service reaches products and stock through the catalog and inventory modules' interfaces, and it refunds the charge if saving the order fails. To keep the layers easy to see, this code leaves out the database transaction and the stock reservation; the request lifecycle in Part 3 adds both.
The Modular Monolith
The Responsibilities and Boundaries lesson compared splitting code by technical job (layers) with splitting it by business area (modules). A modular monolith uses the business-area split at the top level, with modules such as users, orders, payments, catalog, inventory and notifications, and it keeps layers inside each module. Modules call each other only through explicitly defined interfaces, never by reaching into each other's internals.
The code below shows how the orders module reaches the users module. It depends on a small interface that the users module publishes, never on the users table or the user repository.
The key discipline of the modular monolith: each module owns its own tables in the shared database, and no other module queries them directly. The orders module never directly queries the payments table. It calls a method on the PaymentService interface defined by the payments module.
The payments module never directly queries the users table. It calls a method on the UserService interface defined by the users module. These boundaries are not enforced by network topology. They are enforced by code organization, code review, and automated checks that fail the build when one module imports another module's internals (for example import-linter for Python, ArchUnit for Java, or Packwerk with its privacy extension for Ruby).
These boundaries matter because a module with clean boundaries can later be extracted into a separate service without a complete rewrite. Its interface is already defined, its data is already owned by one module, and its dependencies are already declared explicitly. A modular monolith is therefore the safer way to build a system that may later need to be distributed.
Domain-Driven Design in a Monolith
A domain is the business area that the software serves, such as online retail. Domain-driven design (DDD) is an approach that shapes the code around that domain's own concepts and vocabulary, and it applies fully to monolithic architecture. Its central idea is the bounded context, defined in the vocabulary above. In a modular monolith, each module usually maps to one bounded context.
For example, the order context has its own definition of what a "product" means (a line item with a quantity and a price). The inventory context has its own definition of "product": a stock keeping unit (SKU), the store's code for one product variant, with stock levels and reorder thresholds. These two definitions refer to the same real-world object, and keeping them separate prevents inappropriate coupling.
The diagram below shows four bounded contexts inside one application. Under them sits a shared kernel: a small part of the domain model that two or more contexts agree to share and to change only together. Here it holds the domain events, the ID types for users and orders, and a Money type that holds an amount and a currency. The directory layout after the diagram puts one module per business domain. Each module keeps the layers it needs, such as services, repositories and models, and publishes one interfaces file for the other modules to import. The shared kernel gets a folder of its own.
Part 3: The Request Lifecycle in a Monolith
Handling an HTTP Request
Understanding how a request flows through a monolith clarifies both its simplicity and its constraints.
A user submits a "Create Order" request in an e-commerce application:
Step 1: Request arrives at the application server
The request arrives at the application server (Puma for Rails, Gunicorn for Django, Tomcat for Spring Boot, or any other application server). The application server parses the HTTP request and passes it to the framework, whose router then selects the controller from the URL and the HTTP method.
Step 2: Presentation layer processing
The OrdersController receives the request. It extracts the parameters (product IDs, quantities, shipping address), validates the request format (are required fields present? are values the right type?), and authenticates the request (is the session token valid? who is the user?). Authentication in a monolith is usually a function call to the user module's authentication code, not a network call to an authentication service.
Step 3: Business logic layer
The controller calls the OrderService's create_order method, passing the validated parameters and the current user.
The OrderService applies business rules:
- Check that all products exist (calls the catalog module's interface, a function call, which then queries the catalog tables).
- Check that inventory is available (calls the inventory module's interface, a function call).
- Calculate the total price including taxes and discounts (pure domain logic).
- Validate the shipping address (calls AddressValidator, a function call).
- Create the Order domain object.
The stock check here only rejects most out-of-stock orders before the card is charged. It reserves nothing, so two requests can both pass it for the last unit, and Step 5 closes that gap.
Step 4: Payment processing
The OrderService calls the PaymentService to charge the user's card. The PaymentService calls the external payment processor API (this is a network call, unavoidably). If payment succeeds, the PaymentService returns a payment record.
Step 5: Data persistence
The OrderService opens one database transaction. Inside it, the OrderRepository inserts the order and its line items into the shared PostgreSQL database, and the OrderService asks the inventory module, through its public interface, to reserve the stock. The orders module never touches the inventory tables. The inventory module's own code makes that write, and its call joins the transaction that the OrderService opened, so the order, its line items and the stock change are committed together or not at all.
The reservation is one conditional update for each product: it decrements that product's stock only where the stock is at least the ordered quantity, and it fails if no row changed. This conditional update, not the transaction alone, is what stops two orders from both taking the last unit. The early check in Step 3 ran before the transaction began, and even a separate read inside the transaction could let two orders see the same last unit. If the reservation fails, the whole transaction rolls back and the payment from Step 4 is refunded.
Step 6: Event publishing and notifications
After the commit, the OrderService publishes an OrderCreated domain event (within the process, not to a message queue). The NotificationModule listens for this event and enqueues an email notification job. The InventoryModule listens and checks reorder thresholds.
Step 7: Response
The controller receives the created order from the OrderService, serializes it to JSON, and returns it as an HTTP 201 Created response.
Every call between modules in this flow is a function call. The remaining network calls go to infrastructure and external systems: the database, the payment processor, and the job queue that holds the email job. Part 5 shows what the function calls save.
The code below is a shorter create_order that leaves out pricing and payment. It saves the order and asks the inventory module to reserve the stock in one transaction, then publishes a domain event after the commit. The inventory module's reserve method is the conditional update from Step 5, and it runs inside the caller's transaction. The event bus is an object inside the process, so each subscriber receives a plain function call with no broker and no network. Because these calls run after the commit, an error in a subscriber must not fail the order request.
The Transaction Advantage
One of the most practical advantages of the monolithic architecture is visible in Step 5: a single database transaction covers every write to the shared database. If the order record insert succeeds but the stock reservation fails (because the stock ran out or the database returned an error), the entire transaction rolls back. The order record disappears as if it had never been written.
The transaction crosses a module boundary, but the code does not. The orders module never names an inventory table; it calls the inventory module's interface, and both modules use the same open transaction. This is how a modular monolith keeps one transaction across modules while each module still owns its tables. It is also the coupling that extraction must later remove, because once inventory has its own database, the reservation becomes a step in a saga.
A transaction gives atomicity, which distributed systems must work hard to approximate. It does not by itself stop two concurrent requests from acting on the same stale read. That is why Step 5 checks the stock inside the transaction with a conditional update instead of trusting the earlier check.
For the writes inside the transaction, the team needs no compensating logic, no saga and no eventual consistency handling. The work after the commit is different. The in-process event bus gives no durability: if the process stops after the commit and before the publish, the order exists but no email job is queued, and nothing retries. The Event-driven Architecture lesson shows how an outbox table, written in the same transaction as the order, prevents that loss.
Calls to external systems such as the payment processor also sit outside the transaction. If the transaction fails after Step 4 has charged the card, the rollback undoes the database writes but does not refund the payment. That call still needs compensation (a refund), or it must run after the commit.
This simplicity disappears in a distributed architecture. If the order service and inventory service are separate, each should own its own database, because a shared database couples them again.
A write that spans both must either use a commit protocol that coordinates both databases (two-phase commit) or be split into local transactions with compensating steps (a saga). The Microservices Architecture lesson explains the saga pattern, and the Event-driven Architecture lesson shows how a saga is coordinated through choreography or orchestration. For operations that must be atomic across multiple business domains, the monolith's shared transaction is a real advantage.
Part 4: Deployment in a Monolith
The Build and Deploy Pipeline
A monolith has one build pipeline. Any change to any part of the codebase triggers one build, one set of tests, and one deployment.
By default, the entire pipeline runs for every change, although large monoliths often add test selection, which runs only the tests that a change can affect. The outline below lists its stages in order.
A change to the notification module triggers a full build and test of every module. This has costs (slower build times as the codebase grows) and benefits (catching integration issues before deployment rather than discovering them in production after two services are deployed independently).
Rolling and Blue-Green Deployment
A monolith deployed to multiple instances uses rolling deployment or blue-green deployment.
-
Rolling deployment: Instances are updated one at a time (or in batches). At any moment during the rollout, some instances run the old version and some run the new version. Requests are distributed across both versions. Old and new instances must work side by side within the rollout window. They share the database, the cache and the job queue, and one user's requests can reach either version. Each version must therefore accept the data, sessions and queued jobs that the other version creates.
-
Blue-green deployment: Two identical environments run at the same time (blue and green). Traffic goes to blue. The new version is deployed to green and tested. Traffic is switched from blue to green. If problems emerge, traffic is switched back to blue. Blue-green removes the period in which old and new instances serve requests together, at the cost of running double the infrastructure during the deployment window. Both environments usually share one database, however. The schema must therefore work with both versions, and a destructive migration, such as dropping a column that blue still uses, makes switching back unsafe.
Database Migrations in a Monolith
Database schema changes are simpler in a monolith than in a distributed system. A single database serves all modules. A schema migration runs once, and every running instance sees the new schema at the same moment, including instances that still run the old version.
The key constraint is that the old and new versions run against one schema at the same time, both during a rolling deployment and during a blue-green switch and its rollback. If the new application version expects a column that the old version does not write, instances running the old version will insert rows that never set that column. When a change replaces an old column with a new one, the migration strategy must account for this in three steps:
- Expand: Add the new column with a default value, which the old application ignores. Then deploy a version that writes both the old and the new column but still reads the old one.
- Migrate: Backfill the new column, which means filling in its value for every existing row. Then deploy a version that reads the new column, and after it, a version that stops writing the old column.
- Contract: After every instance runs the version that no longer writes the old column, drop the old column in a separate, later migration. Any running version that still wrote the old column would fail once the column is gone.
This three-step pattern (expand, migrate, contract) applies to any breaking schema change made without downtime, such as renaming a column or changing its type, regardless of architecture. It is simpler to reason about in a monolith, where one database serves all modules.
Part 5: The Real Advantages of Monolithic Architecture
Development Velocity at Small Team Size
A team of 5 engineers maintaining one codebase usually ships features faster than a team of 5 engineers maintaining 12 microservices. The reasons are concrete:
There are no network contracts between services. Changing a function signature in a monolith triggers a compile error or test failure that the developer sees immediately. Changing the interface between two microservices requires coordinating the change across two repositories, two deployment pipelines, and potentially two teams.
Debugging does not need distributed tracing. When something goes wrong in a monolith, the stack trace shows the complete call chain. Debugging in a distributed system usually needs distributed tracing, which tags each request with an ID and joins the records that each service writes for it into one view of the request's path.
Cross-cutting changes need no deployment coordination. Adding a new field to a user profile that affects the API, the email notifications, the activity feed, and the payment module is one pull request in a monolith. In a distributed system, it may require coordinating deployments across four services in a specific order to avoid breaking changes.
Simplicity of Operational Model
A monolith has one application to monitor, one set of logs to search, one deployment to track, and one rollback to perform when something goes wrong. This simplicity matters most while the team is small: a new engineer learns one system, not twelve.
The failure modes of a monolith are simpler than those of a distributed system. A monolith has fewer kinds of partial failure: there are no network timeouts between modules and no version mismatches between modules deployed at different times. Partial failures still happen, though. A slow database, a payment API that is down, or a memory leak in one module can degrade every module in the process.
Consistency Through Shared Transactions
The Transaction Advantage section in Part 3 showed this advantage in full: one database transaction can cover the writes of several modules in one business operation. It matters most where data must stay exact, such as payments, inventory and order processing.
Performance of In-Process Communication
The Responsibilities and Boundaries lesson compared the two kinds of boundary crossing: a function call between modules takes under a microsecond, and a network call between services in one data center takes roughly 0.5 to a few milliseconds. The difference adds up over a request. An application that makes 20 calls between modules per request makes 20 function calls, which together take a few microseconds at most. The equivalent distributed system makes 20 network calls, and 20 sequential calls can add 10 to 100 ms of latency.
The advantage grows with the number of interactions between modules in each request. Stack Overflow is a well-known example. For years it served millions of developers from a monolith on a small number of web servers, and its engineers also credited heavy caching and carefully tuned SQL queries for that efficiency.
Easier Refactoring and Feature Evolution
Refactoring across module boundaries is easier in a monolith. Rename a method, move a class to a different module, extract a common abstraction: the IDE's refactoring tools work across the entire codebase, and the compiler or the test suite catches inconsistencies in the same build. In a distributed system, refactoring across service boundaries requires changing the network contract (the API) and coordinating the change across multiple repositories and deployments.
This matters most during the early stages of a product when requirements are uncertain and the domain model is still evolving. Suppose the team discovers that the "project" concept in the product is actually two different things (a template and an instance). As a rough illustration, splitting it might take a day in a monolith and several weeks in a distributed system, where the change crosses service contracts and deployments.
Part 6: What Eventually Breaks in a Monolith
Deployment Coupling
As the team grows and the codebase grows, deployments become more risky. Every deployment includes every change from every engineer on every team.
A bug in one module's change usually means rolling back the entire application, which also removes other modules' changes that were working correctly, unless the change sits behind a feature flag that can be turned off.
Multiple teams owning different parts of the same codebase creates coordination overhead. Team A cannot deploy their payment module changes until Team B's order module changes that depend on the same database migration are ready. Release trains (fixed release dates that every team's changes must meet), feature flags, and deployment freeze windows (periods when no one may deploy) accumulate as coordination mechanisms. In practice, the cost of coupled deployment tends to grow faster than the team itself.
The food-delivery app already shows this cost. Its Fulfillment team wants to release Dispatch daily, but every Dispatch change waits for the shared release twice a week.
Resource Scaling Inefficiency
A monolith deploys as one unit, and by default every copy runs every module. If the image processing module needs more CPU, each added copy of the whole application also brings the memory of every other module, such as the user profile pages, which need no extra capacity. The cost of the extra CPU is therefore paid partly in memory, and the waste grows with the imbalance between modules.
A rough example makes the cost concrete. Suppose each copy of the application needs 4 GB of memory to load and run all of its modules, and only image processing needs more CPU. To triple image processing capacity by adding whole copies, the team grows from 3 copies to 9 and pays for 36 GB of memory. If image processing instead ran in its own processes at about 1 GB each, 3 copies of the application plus 9 image processing processes would need at most 21 GB.
Those separate processes do not require a separate service. One artifact can start in different roles: web processes that serve requests, and a pool of worker processes that run only image processing jobs, fed by a job queue. Each role scales on its own. The Constraints and Trade-offs lesson chose this option for the food-delivery app's Dispatch matching loop, because it scales one module without a second codebase, pipeline or database. A worker started from the same artifact may load more of the codebase than it uses, so its copies can be larger than 1 GB, and it still ships on the shared release schedule.
Technology Lock-In
All modules in a monolith share the same language, runtime, and framework. The image processing module may benefit from a language such as Go or Rust, but it must stay in Ruby or Python, calling native libraries, because that is what the rest of the application uses. The machine learning (ML) inference module, which needs Python's ML libraries, cannot easily be integrated into a Java monolith.
For most applications this constraint is acceptable. For applications that need different technology choices for different components, the monolith's shared runtime is a limitation.
Cognitive Overhead at Large Codebase Size
A codebase with millions of lines of code becomes difficult for any individual engineer to understand in full. Understanding the impact of a change requires understanding the broader system. Long build times, slow test suites, and long time-to-first-contribution for new engineers are symptoms of a codebase that has grown too large for one engineer to understand.
The modular monolith structure mitigates this by allowing engineers to work primarily within their module's boundaries. But at very large scale, even well-organized modules accumulate complexity.
The Monolith Against the Five Quality Attributes
The Quality Attributes lesson rated the monolith on five attributes. The table below gives the reason behind each rating, drawn mainly from Parts 5 and 6.
| Quality attribute | Effect | Why |
|---|---|---|
| Changeability | Neutral | Cross-module changes ship in one pull request, but every change waits for the shared release as teams grow |
| Performance | Help | Calls between modules are function calls, not network calls |
| Availability | Neutral | Copies behind a load balancer add redundancy, but one bad release or memory leak affects every module |
| Security | Neutral | There is one entry point to defend, but every module runs with the same process permissions and database account |
| Cost | Help | There is one pipeline, one set of dashboards and one on-call rotation |
Part 7: When to Stay Monolithic and When to Extract Services
Stay Monolithic When
-
The team is small. As a rough guide, below about 15 to 20 engineers the coordination overhead of microservices usually outweighs the benefit. One well-organized monolith with clear module boundaries serves a small team better than 12 services with their own deployment pipelines, monitoring stacks, and operational runbooks. The food-delivery app's 14 engineers in three teams sit just under that range.
-
The domain is not yet stable. If requirements are changing rapidly and the domain model is still being discovered, the ability to refactor across boundaries cheaply is more valuable than any microservices benefit. Build a modular monolith with clean boundaries and wait until the domain stabilizes before extracting services.
-
Consistency requirements are strong. Operations that must be atomic across modules keep the shared transaction from Part 3, which extraction would replace with a saga or a two-phase commit.
-
Operational simplicity is a priority. A team without dedicated platform engineers or site reliability engineers (SREs, who specialize in keeping production systems running) will struggle to operate 12 services. The operational complexity of microservices is a real cost that requires real investment. If that investment is not available, the monolith is the safer choice.
Extract a Service When
-
A module has a scaling or release need that a worker pool cannot meet. In the Part 6 example, adding whole copies cost 36 GB where separate image processing processes needed at most 21 GB. The first step is a worker pool from the same artifact, and those workers can run on machines of their own, including machines with GPUs. Extraction is justified when that is not enough, for example when the module needs a release schedule of its own. Its dependencies can also justify it, such as large GPU libraries that the shared artifact would carry into every other process.
-
A team needs independent deployment. When a specific team's deployment is repeatedly blocked by other teams' changes, and the waiting slows delivery, independent deployment for that team's module may be justified. This requires the module to have clean boundaries (ideally already in a modular monolith structure).
-
A technology choice brings a clear benefit. If a specific component gains a clear, measured benefit from a different language or runtime (ML inference in Python within a Java monolith, real-time event processing in Go within a Ruby monolith), extraction is justified by the technical requirement.
The food-delivery team wrote its own version of this test into ADR 0007 in the Constraints and Trade-offs lesson. It will reopen the Dispatch service option if Dispatch queries exceed 30 percent of database CPU at the peak, or if Fulfillment releases wait for the shared schedule more than twice in one month.
Before any extraction, check that the team has the supporting infrastructure. That means service discovery so that services can find each other's network addresses, distributed tracing, centralized logging, circuit breakers that stop calls to a failing service, deployment automation, and on-call coverage per service. Without it, extraction creates operational problems worse than the coupling problem being solved.
The Extraction Strategy
When extraction is justified, extract one module at a time, starting with the module that has the clearest boundary and the most compelling reason for independence. The strangler fig pattern is the standard approach: the new service initially runs alongside the monolith, receiving traffic for a subset of functionality. Over time, the service takes on more responsibility and the corresponding monolith code is deleted.
Traffic can reach the new service in two ways. With edge routing, a routing layer such as an API gateway sits in front of the monolith and sends requests for the extracted feature to the new service. With routing inside the monolith, every request still reaches the monolith, and its own code delegates the extracted feature to the new service. This second technique is close to branch by abstraction, which places an interface in front of the old code so that a new implementation can replace it gradually. The diagram after the code below shows edge routing, and the code shows routing inside the monolith.
A modular monolith with clean domain boundaries is much easier to extract from than a big ball of mud, for the reasons Part 2 gave: the module's interface, data and dependencies are already explicit. Extraction then starts from a known boundary instead of a domain modeling exercise, but it is still real work. The module's tables must move to their own database, each function call across the boundary becomes a network call that can fail, and any transaction that spanned the boundary becomes a saga, which the Microservices Architecture lesson explains.
The example below extracts orders, the module this lesson has followed, although it is not the easiest first choice. While the flag is partly on, orders are created in two places, so the team must decide which database holds them and how reports see both. The new path also loses the shared transaction with inventory and needs a saga instead. A module with fewer shared writes, such as notifications, is usually a better first extraction.
The code below routes one operation to the extracted service behind a feature flag. The flag starts with a small share of users and grows, and the monolith path is deleted once the new service handles every request. A comment in the code marks where the new path loses the shared transaction.
FAQs
Q: Is monolithic architecture outdated?
No. It is still a very common way to build software, and it is often the best starting point. Shopify runs a large modular monolith, and Stack Overflow served millions of developers from a monolith for years. What is outdated is the poorly structured monolith with no internal boundaries.
Q: What is the difference between a monolith and microservices?
A monolith is one application deployed as a single unit, where modules talk through function calls and share a database. Microservices split the application into many small services that are deployed separately, talk over the network, and own their own data.
Q: Can a monolith handle millions of users?
Yes. Many copies behind a load balancer handle large traffic, and a worker pool from the same artifact handles heavy background work, as Part 6 showed. The shared database usually becomes the limit first, and the N-Tier Architecture lesson covers how the data tier is scaled.
Q: What is a modular monolith?
A modular monolith is deployed as one unit but is split internally into well-defined modules. Each module has a public interface and owns its own tables inside the shared database. It keeps the single deployment of a monolith and adds the clear module boundaries that make a module easier to extract later.
Q: What is a distributed monolith?
It is a system split into services that must still be changed and deployed together, because they share data or depend on chains of synchronous calls. The result is the operational complexity of microservices without the benefits. It usually appears when services are split before their boundaries are understood, or when they keep sharing one database. The Coupling, Cohesion, and Dependencies lesson defines it, and the Microservices Architecture lesson explains how giving each service its own data removes one of its causes.
Q: Should a startup use microservices?
Usually not at the start. Early-stage startups need to move fast and change direction often. A monolith (ideally a modular one) lets a small team ship quickly. Splitting into services can come later, once the domain and the team are better understood.
Q: How do I migrate from a monolith to microservices?
Use the Strangler Fig pattern. Extract one well-bounded feature into a new service, and send its traffic there through a routing layer in front of the monolith or through delegation inside the monolith's code. Gradually shift traffic to the new service, then remove the old code. Repeat one slice at a time. Avoid full rewrites.
Q: Can a monolith use more than one data store?
Yes. Many monoliths keep their business data in one relational database, but large ones may split it across several databases or shards; Shopify, for example, spreads its monolith's data across many MySQL shards. Most also use a cache such as Redis, a search engine such as Elasticsearch, or object storage for files. The defining trait is that the application code is deployed as one unit, not that only one data store exists.
Key Takeaways
-
A monolith is a single deployable unit where each running copy holds all of the functionality in one process, components communicate through function calls, and the modules usually share one database. It is defined by its deployment model, not by any quality of its internal structure.
-
A modular monolith has clear internal domain boundaries enforced by code organization, code review and automated checks rather than by the network. Modules communicate through defined interfaces, and each module owns its own tables or schema inside the shared database. This is the safer way to build a monolith that may later need to be distributed.
-
A big ball of mud is not the same thing as a monolith. Poor internal structure is a separate problem from the choice of monolithic architecture. A monolith can have excellent internal structure, and a set of microservices can be just as tangled.
-
Calls between modules are function calls. They take nanoseconds, against roughly 0.5 to a few milliseconds for a network call between services, so a request with many calls between modules avoids that cost on every one of them.
-
One transaction can cover several modules' writes. The orders module reaches inventory through its interface, both writes commit together, and a conditional update, not the transaction alone, prevents overselling. In a distributed system, the same operation needs either a two-phase commit across both databases (slow, and blocked while any database is down) or a saga (eventual consistency, visible intermediate states). Calls to external systems sit outside the transaction in both cases.
-
Deployment coupling is a common reason to extract services as teams grow. When teams sharing one codebase block each other's releases and the waiting slows delivery, extracting the blocked team's module can be justified. Extracting before the teams exist adds the coordination cost without the benefit.
-
Most new products should start as a monolith. New products, small teams, and uncertain domains all benefit from the development velocity and operational simplicity of a monolith. The modular monolith with clean domain boundaries gives you a monolith's simplicity at the start and the option to extract services when the need arises.
-
Extraction should follow an observed problem, not anticipated scale. Extract a service when a specific team's deployment is blocked, or when a component's resource needs cannot be met by a worker pool from the same codebase. A technology need that the shared runtime cannot meet is a third reason. Extraction also moves the module's data, turns function calls into network calls that can fail, and turns shared transactions into sagas.
-
In a system design interview, start with one application behind a load balancer and one relational database, and say why. Split out a component only when a stated requirement gives it a different scaling, release or technology need, and name what the split costs.
Reading Progress
0%
On This Page
Know the Vocabulary
Part 1: What Is a Monolith
The Definition
What a Monolith Is Not
Part 2: The Internal Structure of a Well-Designed Monolith
The Layered Structure
The Modular Monolith
Domain-Driven Design in a Monolith
Part 3: The Request Lifecycle in a Monolith
Handling an HTTP Request
The Transaction Advantage
Part 4: Deployment in a Monolith
The Build and Deploy Pipeline
Rolling and Blue-Green Deployment
Database Migrations in a Monolith
Part 5: The Real Advantages of Monolithic Architecture
Development Velocity at Small Team Size
Simplicity of Operational Model
Consistency Through Shared Transactions
Performance of In-Process Communication
Easier Refactoring and Feature Evolution
Part 6: What Eventually Breaks in a Monolith
Deployment Coupling
Resource Scaling Inefficiency
Technology Lock-In
Cognitive Overhead at Large Codebase Size
The Monolith Against the Five Quality Attributes
Part 7: When to Stay Monolithic and When to Extract Services
Stay Monolithic When
Extract a Service When
The Extraction Strategy
FAQs
Key Takeaways