Architecture & Communication
Monoliths & Microservices
A single deployable unit vs. independently deployable, loosely coupled services.
In short
A single deployable unit vs. independently deployable, loosely coupled services.
When designing the boundary limits of software deployment units, engineers face a core choice. Should the entire application be compiled and shipped as a single, self-contained process, or should it be decomposed into smaller, autonomous network services? This is the comparison between Monoliths and Microservices. While a monolith provides simple deployment, low network latency, and transactional safety early on, microservices offer independent deployability, domain isolation, and modular scaling at the cost of distributed complexity.
1. Learning Objectives
- Differentiate between monolithic structures and microservice architectures.
- Analyze the Database-per-Service constraint and how it isolates domains.
- Evaluate the performance costs (network latency, serialization) of network-based RPC boundaries.
- Identify the "Distributed Monolith" anti-pattern and how to avoid it.
- Learn the "Monolith-First" deployment philosophy for greenfield projects.
- Implement a simulation comparing Monolith in-memory execution to Microservice multi-hop REST lifecycles in Java, Python, and C++.
2. Prerequisites
To get the most out of this lesson, you should be familiar with:
- Client-Server Architecture: Dynamic connection sockets and latency.
- N-tier Architecture: Separation of logical layers.
- Distributed Transactions: Sagas and data consistency across nodes.
3. Why This Topic Matters
Starting a new application is simple: a single codebase, a single team, and a single database instance. This monolithic design allows fast feature releases. However, as the organization grows, the monolith reaches its limit:
- Development Bottleneck: 50 developers writing code in the same repository merge conflicts, block deployment pipelines, and step on each other's toes.
- Scaling Skew: A small, CPU-heavy recommendation engine forces you to scale the entire monolithic application node, which consumes memory-heavy database connection resources.
- Technology Lock-In: The entire monolith is written in Java 8. Upgrading to a newer version or using Python for machine learning requires a complete rewrite.
Decomposing the system into microservices allows each team to own, deploy, and scale their service independently, utilizing different languages and databases. However, it introduces distributed complexity: network failures, data consistency issues, and complex tracing.
4. Real-world Analogy
Consider the difference between a Single-Chef Cafe and a Food Court Mall:
The Monolith (Single-Chef Cafe): One chef cooks the appetizers, entrees, desserts, washes the dishes, and rings up the customers. Communications are instant (in the chef's head). If the cafe gets busy, the chef is the bottleneck. If the chef cuts their finger, the entire cafe closes (Single Point of Failure).
Microservices (The Food Court Mall): The mall has separate, independent stalls: a Burger Stall, a Pizza Stall, and a Drink Stall. Each stall has its own kitchen, staff, and inventory. Customers go to different stalls for different items. If the Burger Stall's grill breaks, the Pizza Stall continues serving customers (Fault Isolation). However, ordering a full meal now requires customers to walk to multiple stalls and pay separately, increasing coordinates overhead (Network Latency).
5. Core Concepts
- Monolith: A single, self-contained application. All code components are compiled, packaged, and deployed as a single process (e.g., a
.warfile or a single Go binary). - Microservices: An architectural style that structures an application as a collection of small, loosely coupled, and independently deployable services. Each service is organized around a specific business capability.
- Database-per-Service: A microservice constraint stating that each microservice must own and manage its own private database. Other services can only access its data via API calls; direct database queries from other services are strictly prohibited.
- Polyglot Persistence: Using different database technologies for different services based on their data requirements (e.g. Neo4j for social graph relations, Elasticsearch for search indices, and PostgreSQL for financial transactions).
- Distributed Monolith: An architectural anti-pattern where a system is split into microservices, but the services are so tightly coupled that a change to one requires redeploying all of them. This combines the complexity of distributed networks with the release locks of a monolith.
- Monolith-First: The best practice of starting new systems as modular monoliths first. Once domain boundaries are established and scaling bottlenecks are understood, services can be extracted safely.
6. Visualizations
Monolithic vs. Microservice Architecture
Database-per-Service Isolation
Direct database cross-joins are blocked under microservices; data must be queried through service interfaces:
Request Flow Comparison
7. How It Works Step-by-Step
Let's compare how placing a store purchase is executed in both architectures:
Monolithic Execution Steps:
- The HTTP request reaches the monolith's controller interface.
- The controller invokes
authService.verify()via an in-memory method call. - The code opens a database transaction and queries both the
itemsanduserstables using SQL join queries. - The code writes updates to the
ordersandbalancestables, committing the transaction atomically. - The controller serializes the response and returns it to the client.
Microservices Execution Steps:
- The client request hits the API Gateway.
- The gateway routes the request to the Checkout Service.
- The Checkout Service makes a network HTTP call to the Auth Service to verify the user's session token.
- The Checkout Service makes a network HTTP call to the Catalog Service to verify item availability.
- The Checkout Service writes the transaction to its local Checkout Database.
- The Checkout Service publishes an
OrderCompletedmessage to a message broker, letting the Billing and Shipping services update their databases asynchronously (eventual consistency).
8. Internal Architecture
Decomposing a monolith into microservices requires rewriting internal code structures:
- Monolith Internals: Organized as logical folders (Controller, Service, Repository) within a single compile target. All dependencies are solved by compiler linking.
- Microservices Internals: Organized as isolated codebases, each running its own process. Services communicate using lightweight protocols (REST/JSON over HTTP, gRPC over HTTP/2, or AMQP/Kafka event streams). A Service Discovery engine (like Consul or Eureka) tracks node IPs dynamically.
9. Request Lifecycle
Let's trace a user request to update their profile settings:
- Gateway routing: The client POST request hits the API Gateway. The gateway validates the JWT signature and routes the request to the User Profile Microservice.
- Local Write: The User Profile Service receives the request, updates the user's details inside its private MySQL database, and returns
200 OKto the gateway. - Async Propagation: The service publishes a
UserProfileUpdatedmessage to a Kafka topic. - Consuming updates: The Search Recommendation Service and Email Service consume the message from Kafka, updating their local indexes and sending a confirmation email asynchronously.
10. Deep Dive
A. The Database-per-Service Constraint
In a monolithic system, joining tables across domains is easy:
In microservices, this SQL join query is strictly forbidden. The users table belongs to the Auth DB, and the orders table belongs to the Order DB. The Order Service cannot connect to the Auth DB.
To join this data, the Order Service must query the Auth Service over the network (GET /users/{id}) and merge the records in application memory. This is slower and consumes more memory, but it prevents database coupling, allowing each database schema to evolve independently.
B. The "Distributed Monolith" Anti-Pattern
If you split your monolith into microservices but keep the services tightly coupled using synchronous HTTP calls, you build a Distributed Monolith.
If Service A calls Service B, which calls Service C, and a change to C's API requires updating and deploying A and B at the same time, you lose the benefits of microservices. You now have the deployment locks of a monolith combined with the network latency and failure risks of a distributed system.
To prevent this, services must be designed to be autonomous. Use asynchronous messaging (event-driven architecture) or local cache replication to eliminate synchronous dependencies.
C. Monolith-First Philosophy
Many start-up engineering teams choose microservices on day one because they expect rapid growth. This is usually a mistake.
Early in a project, business requirements and domain boundaries change quickly. Splitting a system before understanding these boundaries results in services that are poorly aligned to business domains, leading to heavy refactoring across network boundaries.
The recommended best practice (coined by Martin Fowler) is Monolith-First: build the application as a clean modular monolith first. Once the business model stabilizes and scaling bottlenecks emerge, extract specific modules into independent microservices.
11. Production Examples
- Netflix: Migrated from a monolithic DVD rental system to a microservices architecture between 2008 and 2016. Today, Netflix operates thousands of microservices, allowing them to scale streaming video delivery to hundreds of millions of users globally.
- Amazon: In 2002, CEO Jeff Bezos issued the famous "API Mandate," requiring all internal software teams to expose their data and functionality through service APIs and prohibiting direct database sharing. This split Amazon into autonomous, "two-pizza" teams, paving the way for AWS.
12. Advantages
Microservices Advantages
- Independent Deployability: Teams deploy updates to their services without waiting for other teams, speeding up releases.
- Polyglot Technology Stack: Choose the best technology for each service (e.g. Go for high-performance APIs, Python for machine learning).
- Blast Radius Isolation: If the Promo Service crashes, the core Checkout and Auth services continue to function.
Monolith Advantages
- Zero Network Overhead: Calls between modules are fast, local memory operations.
- Transactional Consistency: Native ACID transactions guarantee data integrity across all tables.
- Operational Simplicity: One build pipeline, one VM target, and one database to monitor.
13. Limitations
Microservices Limitations
- Distributed Complexity: Network partitions, latency overhead, and tracing require dedicated infrastructure support.
- Data Inconsistency: Lack of ACID transactions requires implementing complex eventual consistency patterns (like Sagas).
Monolith Limitations
- Scaling Inefficiency: You must scale the entire application, even if only one module is experiencing load.
- Large Blast Radius: A memory leak or crash in any module crashes the entire application process.
14. Trade-offs
Code Simplicity vs. Organizational Scaling
A monolith is simpler to develop, test, and deploy, making it ideal for small teams (1-15 developers). However, as the engineering team grows to 100+ developers, the monolith becomes a coordination bottleneck. Microservices trade code simplicity for organizational scaling, allowing independent teams to build and deploy features autonomously.
15. Performance Considerations
- Network Hop Latency: Replacing in-memory calls with network APIs increases request latency. Use persistent connections (Keep-Alive) and keep network call chains short.
- Data Serialization Costs: Converting data to JSON/Protobuf at each microservice boundary consumes CPU cycles. gRPC/Protobuf is preferred in high-throughput environments.
16. Failure Scenarios
- Cascading Network Timeouts: If Service C is slow, Service B's threads block waiting for C, causing Service A to time out.
Mitigation: Configure strict request timeouts, client retries with jitter, and circuit breakers. - Distributed Write Failures: An action requires updates across two services. The write to Service A succeeds, but the write to Service B fails.
Mitigation: Implement Saga orchestrators to coordinate compensating transactions (rollbacks) asynchronously.
17. Best Practices
- Enforce strict Database-per-Service boundaries.
- Use correlation IDs in all requests to trace logs across service boundaries.
- Implement circuit breakers and fallback handlers for all inter-service network calls.
- Start with a modular monolith first, extracting microservices only when scaling pain is real.
18. Common Mistakes
- Allowing different microservices to connect to the same shared database, which couples schemas.
- Decomposing the system into too many tiny services (nanoservices), which increases network latency and configuration complexity.
19. Implementation (Monolith vs. Microservice Simulator)
Below is a complete, production-grade Monolith vs. Microservices Request Simulator. It models both architectures, comparing the latency and execution paths of an in-memory monolith process to a decomposed microservice cluster communicating over simulated REST network hops.
20. Interview Questions & Answers
Q1. Why is direct database sharing across microservices considered an anti-pattern?
Answer: Direct database sharing couples microservices at the database schema level. If Service A queries Service B's tables directly:
- A database schema change in Service B (like modifying a column name) breaks Service A instantly, requiring coordinated deployments.
- Service B cannot optimize its database engine (e.g. migrating from PostgreSQL to MongoDB) without forcing Service A to rewrite its database drivers.
- Locking database tables can create lock contention across domains.
Instead, microservices must adhere to the Database-per-Service constraint, accessing data exclusively through service API interfaces.
Q2. What is a "Distributed Monolith" and how do you avoid it?
Answer: A Distributed Monolith is a system that has been physically decomposed into separate microservices but keeps them tightly coupled using synchronous HTTP or gRPC API calls.
If Service A calls Service B, which calls Service C synchronously, and a crash or slowdown in C cascades up to take down A, the services are not autonomous. This architecture introduces the complexity of distributed systems (network failures, tracing) without the independent deployability benefits of microservices.
To avoid this:
- Use Asynchronous Messaging (Event-Driven Architecture) to communicate between services.
- Replicate hot lookup data locally inside service caches to eliminate real-time network calls.
- Establish clean boundaries based on Bounded Contexts (Domain-Driven Design).
Q3. Why is the "Monolith-First" philosophy recommended for new systems?
Answer: At the start of a project, the business model and domain boundaries change quickly. Decomposing the system into microservices early results in services that are poorly aligned to business domains. This leads to heavy refactoring across network boundaries.
Building a modular monolith first allows you to establish clean domain boundaries in code (using Java packages or Python modules). Once the domain model stabilizes and scaling bottlenecks emerge, you can extract these modular folders into independent microservices safely.
21. Practice Exercises
- Exercise 1 (Easy): Sketch a diagram contrasting the database access models of a Monolith (shared schema) versus Microservices (isolated database-per-service).
- Exercise 2 (Medium): Modify the provided Python simulation to simulate a Network Timeout in the
CatalogService. If a timeout occurs, theCheckoutMicroserviceshould catch the exception and return a cached placeholder detail instead of crashing. - Exercise 3 (Hard): Implement a Python script simulating Polyglot Persistence. Build a microservice environment where the Auth Service writes to an SQL database, the Catalog Service reads from an Elasticsearch index, and the Recommendation Service queries a Neo4j graph database, comparing performance across queries.
22. Challenge Problem
The E-Commerce Microservice Decomp Challenge: You have a monolithic e-commerce application processing 10,000 orders per minute. You need to extract the Checkout and Notifications logic into separate microservices.
Draft a comprehensive migration plan detailing:
- How to split the shared monolithic SQL database into two isolated databases, migrating the
ordersandnotificationstables without data loss or system downtime. - The data synchronization strategy (e.g. dual-writing or Change Data Capture (CDC)) used during the transition.
- How you handle reporting queries (which previously joined users, orders, and notifications tables) after the database is split.
23. Summary
Choosing between monoliths and microservices is a fundamental architectural decision. Monoliths offer deployment simplicity, low network latency, and transactional safety, making them ideal for early-stage applications. Microservices trade this simplicity for organizational scaling, fault isolation, and independent deployability, requiring robust infrastructure support to manage distributed networks.
24. Cheat Sheet
| Feature | Monolith | Microservices | Nanoservices |
|---|---|---|---|
| Deployment Target | 1 single compilation package. | Many independent packages (1 per domain). | Dozens of tiny deployment functions (e.g. AWS Lambda). |
| Database Axis | Single shared database. | Strict Database-per-Service. | Often shares a schema, causing coupling. |
| Inter-service Latency | Sub-nanosecond (local memory). | Moderate (network HTTP/gRPC roundtrips). | High (frequent network hops between tiny functions). |
| Blast Radius | Large (a crash in one module takes down all). | Small (failure is isolated to the service). | Small (isolated to the function). |
25. Quiz
1. What does the "Database-per-Service" constraint prohibit in microservices?
- A. Using MySQL databases.
- B. Direct database connection or query joins from one service to another service's private database.
- C. Storing data in JSON format.
- D. Running databases in private subnets.
Answer: B. Database schema isolation is mandatory; inter-service data queries must proceed through API endpoints.
2. What is a "Distributed Monolith"?
- A. A database partitioned across multiple sharded nodes.
- B. A microservice system where services are tightly coupled, forcing coordinated deployments.
- C. A monolith deployed on multiple virtual machines.
- D. An integration bus connecting legacy SOAP services.
Answer: B. Tight coupling in microservices creates a distributed monolith, combining network latency with monolithic deployment locks.
3. Why is the "Monolith-First" philosophy recommended for new projects?
- A. Monoliths are faster to execute in production.
- B. To establish stable domain boundaries in code before extracting them as services, avoiding network-level refactoring.
- C. Monoliths do not require database connections.
- D. Microservices are deprecated.
Answer: B. Designing boundaries in code first is simpler and avoids the complexity of modifying split networks during early-stage prototyping.
4. Which of the following is an advantage of monolithic architecture?
- A. Polyglot persistence.
- B. Low request latency due to local, in-memory method execution.
- C. Independent scaling of specific modules.
- D. Isolated blast radius.
Answer: B. In-memory method execution has no network latency or serialization overhead.
5. What does "Polyglot Persistence" mean?
- A. Writing code in multiple programming languages.
- B. Utilizing different database engines (e.g. Graph, Relational, Document) optimized for each service's specific requirements.
- C. Deploying databases in multiple geographic regions.
- D. Encrypting password hashes.
Answer: B. Storing data in specialized database engines based on query patterns.
6. What is a key performance penalty introduced by microservices?
- A. Database index bloat.
- B. Network latency hops and serialization overhead between service boundaries.
- C. Slower local CPU clock speeds.
- D. Decreased client side load speeds.
Answer: B. Traversing network sockets and serializing/deserializing payloads introduces latency overhead compared to in-memory calls.
7. How does a microservice architecture isolate failures?
- A. By running all code on a single thread.
- B. By ensuring a failure in one service does not crash other healthy services (blast radius isolation).
- C. By replicating databases synchronously.
- D. By blocking all client network connections during errors.
Answer: B. Independent deployment and processes prevent a failure in a secondary service from taking down core application nodes.
8. What is a common mistake when migrating from a monolith to microservices?
- A. Splitting the database first.
- B. Keeping a shared database across services, which couples schemas.
- C. Using gRPC for internal communications.
- D. Deploying services to Kubernetes containers.
Answer: B. Shared databases couple microservices at the database schema level, preventing independent deployments.
9. Which organization famously migrated to microservices using "two-pizza" teams?
- A. Oracle.
- B. Netflix.
- C. Amazon.
- D. Google.
Answer: C. Amazon decomposed its monolithic systems into autonomous, two-pizza teams, creating a service-oriented model.
10. What metrics comparison is verified in the implementation simulator?
- A. Database query size.
- B. CPU clock cycles.
- C. In-memory execution latency vs. multi-hop network latency.
- D. DNS resolution speed.
Answer: C. The simulator measures and prints the timing difference between local monolith calls and microservice network hops.
26. Further Reading
- Building Microservices — Sam Newman.
- Microservices Guide — Martin Fowler and James Lewis.
- Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans.
27. Next Lesson Preview
We have seen how microservices decouple deployment boundaries but introduce distributed data consistency challenges. In the next lesson, we will explore Event-Driven Architecture (EDA)—the communication pattern that lets microservices coordinate actions asynchronously by publishing and consuming events, eliminating tight API coupling.
Key takeaways
- Monolith = simple early; microservices = scalable but distributed.
- Each microservice owns its own data and deploys independently.