To properly understand what microservices bring, you have to understand equally well what they are being compared with: the monolithic architecture. It is the most common way of building software, the one TechCorp uses today and, most likely, the one you have used in most of your projects. Far from being a relic, the monolith has important virtues and, when well built (the so-called modular monolith), it is an excellent choice for a great many systems.
In this lesson we will define what a monolith is and its variants, compare both architectures dimension by dimension with a table and diagrams, and follow step by step the lifecycle of the same functional change ("adding discount coupons to TechCorp's store") in each of them to see where the real differences lie. We will finish by dismantling the myth that the monolith is always the bad option. We will not yet decide here when one or the other is appropriate (that comes in lesson 01-04), nor look at the technique of splitting a monolith (lesson 02-02).
Contents
- What a monolith is
- The modular monolith: a middle ground
- Dimension-by-dimension comparison
- The two systems in diagrams
- One change, two lifecycles: discount coupons at TechCorp
- Other alternatives: SOA and the modular monolith as a destination
- The myth that the monolith is always bad
- Common mistakes and tips
- Exercises
- Conclusion
- What a monolith is
A monolith is an application built and deployed as a single unit: one process (or one artifact running as several identical processes), usually one code repository, and almost always one database. All the functionality (interface, business logic, data access) lives in the same deployment.
Typical characteristics:
- One deployment unit: everything is compiled or packaged together and shipped together. At TechCorp,
techcorp-shopis a single Node.js project started withnode server.jsthat serves the whole store. - In-memory internal calls: when the orders module needs to check stock, it calls a function of the inventory module in the same process. Nothing travels over the network.
- One shared database: all the tables (
customers,products,stock,orders,payments,notifications) live in the same PostgreSQL and any module can query any table. Transactions span whatever they need to. - Horizontal scaling by full replica: if more capacity is needed, several identical copies of the monolith run behind a load balancer.
It is important to stress that "monolith" does not mean "messy code". A monolith can be perfectly organized into layers and modules. When a monolith's code is tangled, with no clear internal boundaries, people talk about a big ball of mud, which is a code quality problem, not a deployment architecture one.
- The modular monolith: a middle ground
A modular monolith is a monolith whose code is divided into modules with explicit boundaries, each with its internal API and, in the most disciplined version, with its own schema or set of tables that the other modules do not access directly. It is still deployed as one unit, but on the inside it looks a lot like a set of microservices living in the same process.
| Aspect | "Classic" monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment unit | One | One | One per service |
| Boundaries between areas | Blurry or nonexistent | Explicit (modules with an internal API) | Explicit (processes with a network API) |
| Data access | Any module reads any table | Each module accesses only its own tables | Each service has its own database |
| Internal communication | Unrestricted function calls | Function calls through the module's API | HTTP calls or messages |
| Network cost | None | None | Present in every interaction |
The modular monolith has a double value: it is an excellent architecture in its own right, and it is also the best starting point if you ever want to migrate to microservices, because the boundaries are already drawn. A well-isolated module can be "extracted" into a service with relative ease; a ball of mud cannot. We will come back to this idea in module 2.
- Dimension-by-dimension comparison
This is the central table of the lesson. It compares both architectures along the dimensions that matter most in practice. Read it calmly; almost every row corresponds to a later lesson in the course.
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment | One unit. Any change, however small, means deploying the whole application. Less frequent, "bigger" deployments. | One unit per service. Each team deploys its service whenever it wants. Small, frequent deployments, but many more pipelines to maintain. |
| Scaling | The whole application is replicated even if only one part needs capacity. Simple but inefficient. | Each service is scaled independently according to its load. Efficient, but it requires orchestration and per-service load balancing. |
| Data | One shared database. ACID transactions across any tables. Immediate, simple consistency. | One database per service. No global transactions; eventual consistency and patterns such as sagas. Each service picks its own engine. |
| Teams | Several teams work on the same code and the same deployment; they step on each other and need to coordinate. | Each team owns its services end to end. Less day-to-day coordination, but common governance is needed. |
| Technology | A single stack for the whole application. Upgrading a framework version affects everything. | Each service can use its own stack. Freedom, in exchange for more technologies to maintain. |
| Testing | End-to-end tests are easy: start the application and test. Suites grow and become slow. | Testing an isolated service is fast. Testing the integration of many services is hard: contract tests and complex environments are needed. |
| Performance | In-memory internal calls, almost free. SQL queries with JOIN across any tables. |
Every interaction between services crosses the network: latency, serialization, possible failures. "Joins" across data from different services have to be done in the application. |
| Fault tolerance | A serious error (memory leak, unhandled exception) can take down the whole application. | A downed service does not have to take down the others, if resilience is designed well. But there are many more points of failure. |
| Initial cost | Low. One project, one server, one database. You start delivering value in days. | High. You need containers, orchestration, messaging, observability, CI/CD per service... before the first feature is in production. |
| Complexity | It lives in the code: as it grows, it becomes hard to understand and change. | It lives in the interactions: each service is simple, but the whole is a distributed system. |
| Debugging | One process, one log, one debugger. | Many processes, many logs; distributed tracing is needed to follow a request. |
A useful way to read the table: the monolith is simple at first and gets complicated as it grows; microservices are complicated at first and keep complexity bounded as they grow. The question of where the crossover point lies is exactly what we will tackle in the next lesson.
- The two systems in diagrams
TechCorp's monolith (current situation)
flowchart TB
Customer[Browser / Mobile app]
subgraph techcorp-shop monolith (Node.js + Express, a single process)
direction TB
R[Express routes]
subgraph Modules
MC[customers]
MCat[catalog]
MI[inventory]
MO[orders]
MPa[payments]
MN[notifications]
end
R --> MC & MCat & MI & MO & MPa & MN
MO --> MI
MO --> MPa
MO --> MN
MO --> MCat
end
DB[(Single PostgreSQL<br/>all tables)]
Customer --> R
MC & MCat & MI & MO & MPa & MN --> DB
Everything lives inside the same rectangle. The arrows between modules are function calls. All modules point to the same database.
The same store as microservices (the course's destination)
flowchart TB
Customer[Browser / Mobile app]
GW[API Gateway :8080]
Customer --> GW
subgraph Services
CUS[customers-service :3004]
CAT[catalog-service :3001]
INV[inventory-service :3006]
ORD[orders-service :3002]
PAY[payments-service :3003]
NOT[notifications-service :3005]
end
DBC[(PG customers)]
DBCat[(MongoDB catalog)]
DBI[(PG inventory)]
DBO[(PG orders)]
DBPa[(PG payments)]
MQ[(RabbitMQ)]
GW --> CUS & CAT & ORD
CUS --> DBC
CAT --> DBCat
INV --> DBI
ORD --> DBO
PAY --> DBPa
ORD -. events .-> MQ
MQ -. events .-> INV & PAY & NOT
Each service has its own rectangle and its own database. Relationships between services go over the network (HTTP or RabbitMQ). The gateway is the only point of contact with the outside world.
- One change, two lifecycles: discount coupons at TechCorp
Nothing illustrates the differences better than following a real change. Marta asks for a new feature: discount coupons. A customer enters a code when placing the order, the system validates the coupon, applies the discount to the total and records that the coupon has been used. In addition, the catalog must be able to show which products accept coupons, and a different email must be sent when the order carries a discount.
5.1 In the monolith
| Step | What happens |
|---|---|
| 1. Design | A developer (or two) designs the coupons table and decides where the logic goes. Since everything is in the same code, they can touch whatever they want. |
| 2. Database | They add a migration: a coupons table (code, discount, expiry date, used) and a coupon_id column in orders. One migration, one database. |
| 3. Code | They modify createOrder to accept couponCode, run a SELECT on coupons, validate, apply the discount and mark the coupon as used inside the same transaction that creates the order. They modify the catalog query to include accepts_coupon. They modify the email template. |
| 4. Testing | They start the whole application locally, create an order with a coupon and check it end to end. The automated tests for the entire application take 25 minutes because there are so many. |
| 5. Coordination | Although one person makes the change, it touches the orders, catalog and notifications areas. The owners of those areas review the pull request; sometimes there are conflicts with other in-flight changes to the same files. |
| 6. Deployment | The change goes into the weekly Friday deployment together with everything else. The complete application is deployed. If something breaks anywhere else, everything is rolled back, coupons included. |
| 7. Operation | If the discount calculation has a bug, they search the single log and fix it for the following Friday (or do an emergency deployment of the whole application). |
Bottom line: fast, simple development (one transaction, one repository, one deployment), but tied to the global deployment calendar, at risk of stepping on other changes, and with a deployment that drags everything along.
5.2 In microservices
| Step | What happens |
|---|---|
| 1. Design | First question: who owns the "coupons" capability? Is it a new service (promotions-service) or part of orders-service? Suppose the team decides it is a new service, because marketing will want to manage campaigns. Its API (GET /coupons/{code}, POST /coupons/{code}/redemptions) and its events have to be defined. |
| 2. Database | promotions-service creates its own database with the coupons table. orders-service adds couponApplied to its own schema. Two migrations, in two databases, with no common transaction. |
| 3. Code | orders-service changes createOrder to call promotions-service over HTTP to validate the coupon, apply the discount and, after creating the order, request the redemption. You have to decide what happens if promotions does not respond (is the order rejected? is the coupon ignored?). catalog-service adds acceptsCoupon to its document and its API. notifications-service listens to order.confirmed, which now includes discountApplied, and picks the right template. |
| 4. Contracts | The catalog-service API and the order.confirmed event change: this must be done in a backward-compatible way (adding fields, not removing them) so that consumers that have not been updated do not break. |
| 5. Testing | Each service tests its own part in seconds. On top of that, contract tests between orders and promotions and an integration test with the services involved running together. |
| 6. Coordination | Three or four teams take part, but each works on its own service. Coordination is about contracts, not code: "which fields does GET /coupons/{code} return?" |
| 7. Deployment | First promotions-service is deployed (new, breaks nothing). Then catalog and notifications (compatible changes). Finally orders, which switches the feature on. Each deployment is small and independent and can happen on any day. |
| 8. Operation | If the discount is computed wrongly, it is fixed and only orders-service is redeployed, in minutes. If the bug is "the coupon was marked as used but the order was not created", you have to inspect the trace across two services and perhaps add a compensation. |
Bottom line: more up-front work (defining the service, API, contracts, failure handling), but independent deployments, teams that do not step on each other, and a business capability (promotions) that is born already isolated and will be able to evolve on its own.
5.3 What the walkthrough teaches
flowchart LR
subgraph Monolith
A1[Quick design] --> A2[One migration] --> A3[One big PR] --> A4[Slow tests] --> A5[Weekly deployment of everything]
end
subgraph Microservices
B1[Design: who owns it?] --> B2[Define contracts] --> B3[Several small PRs] --> B4[Fast tests + contract] --> B5[Independent deployments]
end
- The monolith optimizes developing the change; microservices optimize delivery and independent evolution.
- In the monolith the difficulty shows up at the end (coordinating the deployment, rolling everything back). In microservices it shows up at the beginning (deciding boundaries and contracts).
- Failure handling, which the transaction solves in the monolith, is an explicit design decision in microservices.
- Other alternatives: SOA and the modular monolith as a destination
The choice is not binary. Between "one monolith" and "dozens of microservices" there are valid intermediate stops:
- Modular monolith (seen in section 2): same deployment, rigorous internal boundaries. Many organizations should aim for this and stay here. It is also the ideal foundation for extracting services in the future.
- "Classic" SOA: coarser-grained services than microservices, often with an integration bus. Still common in large corporations. It shares with microservices the idea of services, but with more centralization (see lesson 01-01).
- A few large services ("macroservices"): splitting the system into two or three pieces for very specific reasons (for example, separating the read-heavy catalog from the rest) without going any further. This is a pragmatic strategy that captures part of the advantages at a fraction of the cost.
None of these options is "cheating". They are different points on the same spectrum, and the sensible thing is to pick the one that addresses the real problems you have.
- The myth that the monolith is always bad
This is worth saying plainly, because the industry sometimes forgets it: the monolith is not an architectural mistake. It is the correct default for most new projects and for many mature systems. Its advantages are real:
- You start delivering value in days, not after months of infrastructure.
- A developer can understand and run the whole system on their laptop.
- Transactions and
JOINs solve for free problems that in microservices cost complex patterns. - Refactoring is easy because the compiler or the tests see all the code at once; moving a responsibility from one module to another means changing an
import, not renegotiating a contract between teams. - Operating it is cheap: one process, one database, one log.
Large, highly respected companies operate huge monoliths successfully, and others that migrated to microservices have partially gone back to consolidating services because the operational cost did not pay off. Microservices do not make a system good; they solve a specific set of problems (uneven scaling, teams getting in each other's way, slow deployment cycles) in exchange for another set of problems. If you do not have the former, do not buy the latter.
The TechCorp case we follow in the course was chosen precisely because it does have those problems: campaigns that saturate only the catalog, weekly deployments made in fear, teams stepping on each other in the same code. That is why its story is a migration story. But a four-person TechCorp with a thousand orders a month should stay on its monolith, ideally a modular one, and be happy.
Common Mistakes and Tips
- Confusing monolith with bad code. A monolith can be impeccably organized. If the problem is that the code is chaos, the solution is to refactor toward a modular monolith, not to spread the chaos across the network.
- Comparing a real monolith with idealized microservices. It is easy to win the debate if you compare the legacy monolith full of technical debt with a clean services diagram. Compare with the same honesty: microservices accumulate debt too, just distributed.
- Forgetting the initial cost. The comparison table shows that microservices pay up front. If the project needs to deliver value in weeks, that cost can be prohibitive.
- Thinking that transactions are simply "replaced". Moving from "one transaction that does everything" to "several services with no common transaction" is the hardest part of the comparison and deserves the most attention in the design.
- Tip: when evaluating a functional change, do the exercise from section 5 in your head: walk through the steps in your current architecture and in the alternative. It is the fastest way to spot where the real costs are.
Exercises
Exercise 1: Classifying architectures
For each description, state whether it is a classic monolith, a modular monolith or microservices, and justify your answer using the table in section 2.
- A Node.js application in a single repository, divided into
customers/,catalog/andorders/folders, each with its own set of tables that the other folders access only through the module's public functions; it is deployed as a single process. - Six independent Node.js processes, each with its own database, that communicate over HTTP and RabbitMQ and are deployed separately.
- A Java application deployed as a single
.warfile in which any class can run SQL queries against any table.
Exercise 2: Walking through a change
Marta asks for another feature: a wishlist (a customer can save products for later and receive an email if their price drops). Describe, as a list of steps like in section 5, how it would be implemented in TechCorp's current monolith and in the microservices version. Point out the most expensive step in each.
Exercise 3: Defending the monolith
A colleague claims: "Monoliths are a thing of the past; any serious project today should start with microservices". Write three arguments, backed by the table in section 3, to rebut them.
Solutions
Exercise 1
- Modular monolith. One deployment unit, but with explicit boundaries between modules and "private" data per module. It is exactly the middle ground described.
- Microservices. Several deployment units, one database per service, network communication.
- Classic monolith (and probably drifting toward a ball of mud). One deployment unit and indiscriminate data access.
Exercise 2 (one possible solution)
Monolith:
- Migration:
wishlist (customer_id, product_id, price_at_time)table. - Endpoints in the customers routes to add and list wishes, with a
JOINtoproductsto show the name and current price. - When a product's price is updated (catalog module), query
wishlistand call the notifications function to send the email, all in the same process. - Tests for the complete application; weekly deployment. Most expensive step: the joint deployment and the slow tests; the development itself is simple.
Microservices:
- Decide where the wishlist lives: probably in
customers-service(it is a customer preference). customers-serviceadds its table and endpoints; to show the name and current price it must callcatalog-serviceor keep a local copy updated through events.catalog-servicepublishes aproduct.price-changedevent;customers-serviceconsumes it, looks up who had that product on their list and publishes something likewish.price-drop;notifications-serviceconsumes that and sends the email.- Contracts: define the two new events and their fields.
- Independent deployments of catalog, customers and notifications, in that order. Most expensive step: deciding how customers obtains the product data (synchronous call versus event-driven copy) and designing the event chain; failures and duplicates have to be thought through.
Exercise 3 (possible arguments)
- Initial cost: a monolith delivers value in days with one server and one database; microservices demand containers, orchestration, messaging and observability before the first feature. For a new project, whose biggest risk is not finding a market, that cost is hard to justify.
- Data and performance: in a monolith, a transaction and a
JOINsolve problems that in microservices require sagas, data duplication and network calls with latency. While the domain is not yet clear, that simplicity is worth its weight in gold. - Teams: the autonomy advantages of microservices only appear when there are several teams getting in each other's way. With a single small team there is nobody to stop getting in the way of, and there are many more services to operate. Besides, a modular monolith offers clear boundaries at no network cost and leaves the door open to extracting services later.
Conclusion
In this lesson we defined the monolith as a single deployment unit with a shared database and in-memory internal calls, and we presented the modular monolith as a disciplined variant that combines the monolith's operational simplicity with clear internal boundaries. The dimension-by-dimension comparison showed a consistent pattern: the monolith is cheap and simple at first and gets complicated as it grows, whereas microservices pay a high initial cost in exchange for keeping the complexity of each piece bounded and enabling independent deployments, scaling and teams. Walking through the "discount coupons" change made tangible where the difficulties appear in each case: at the end of the cycle in the monolith, at the beginning in microservices. And we made it clear that the monolith is not a second-class architecture: it is the right choice for many systems.
With this comparison in hand, we can now tackle the question that really matters in practice: when should you adopt microservices and when should you not? The next lesson gives concrete criteria, warning signs, prerequisites and a checklist for making that decision on solid ground.
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
