Why Monoliths Are Underrated in a Microservices World
Why Monoliths Are Underrated in a Microservices World
The pendulum of software architecture swings hard, and for the past decade, it has swung decisively toward microservices. Conferences, blog posts, and hiring practices have all championed the modular, independently deployable, and scalable architecture. But in the rush to decompose every system into dozens of tiny services, many teams have discovered that the cure can be worse than the disease. The monolithic architecture, often dismissed as antiquated, deserves a closer look. It is not a relic; it is a pragmatic choice for a wide range of real-world scenarios.
The Hidden Costs of Distributed Systems
The primary argument for microservices is that they allow teams to work independently and scale components individually. However, this comes at a significant cost that is often underestimated during the initial design phase. A distributed system introduces network latency, partial failures, and the need for complex inter-service communication patterns.
Consider a simple e-commerce application. In a monolith, a single function call retrieves a user's profile, their order history, and the inventory status of their cart. In a microservices architecture, this same operation requires multiple synchronous HTTP calls to the user service, order service, and inventory service. If any of these calls fail, the entire operation must be handled with retries, circuit breakers, and fallback logic.
The operational complexity is not trivial. Teams must manage service discovery, API gateways, distributed tracing, and eventual consistency. A study by the University of Cambridge and Netflix found that the probability of a cascading failure increases exponentially with the number of services. For a system with 10 services, the risk is manageable. For a system with 100 services, the operational overhead can consume more than 40% of engineering time.
When a Monolith Makes More Sense
A monolith is not a single, tangled block of code. It is a single deployable unit with well-defined internal modules. The key insight is that a well-structured monolith can provide the same logical separation as microservices without the distributed systems overhead.
The most compelling case for a monolith is when the domain is cohesive and the team is small. For example, a startup building a content management system does not need separate services for user authentication, content storage, and analytics. A single codebase with clear module boundaries allows the team to iterate quickly, deploy with confidence, and avoid the cognitive load of managing multiple running processes.
Another scenario is when the system requires strong consistency. Financial applications, inventory management, and booking systems often rely on ACID transactions. In a microservices architecture, achieving strong consistency across services requires distributed transactions, two-phase commits, or sagas, all of which are complex and error-prone. A monolith with a single database can enforce constraints with a simple transaction, reducing the risk of data corruption.
The Modular Monolith: A Practical Middle Ground
The most underrated pattern in modern software architecture is the modular monolith. This approach structures the application as a single deployable unit but enforces strict module boundaries. Each module has its own public API, internal implementation, and data access layer. The modules are decoupled at the code level but share the same process and database.
Consider a simple example in Java using a package-based structure:
// Module: UserManagement
package com.example.user;
public class UserService {
public User createUser(String name, String email) {
// Business logic
return userRepository.save(new User(name, email));
}
}
// Module: OrderManagement
package com.example.order;
public class OrderService {
public Order createOrder(Long userId, List<Item> items) {
// Business logic, calls UserService via internal API
User user = userService.findById(userId);
// Process order
}
}
In this pattern, modules communicate through well-defined interfaces, not through HTTP calls. This eliminates network latency and partial failure concerns. The modules can be extracted into microservices later if needed, but only when the need is proven by metrics, not by speculation.
The modular monolith also simplifies testing. Integration tests can be run in a single process without mocking network calls. End-to-end tests can be written with a single database connection. This drastically reduces the feedback loop for developers.
The Pragmatic Path: Start Monolithic, Extract When Necessary
The most successful microservices adoptions I have observed did not start with microservices. They started as monoliths. The team built the product, validated the market, and only decomposed the monolith when a clear bottleneck emerged.
For example, a team building a video streaming platform might start with a monolith. As the user base grows, they notice that the video encoding module is CPU-bound and cannot scale horizontally with the rest of the application. This is the moment to extract that module into a separate service. The rest of the application remains monolithic, which is perfectly fine.
This incremental approach avoids the premature optimization trap. It also prevents the "distributed monolith" anti-pattern, where services are deployed separately but are so tightly coupled that they cannot function independently. By starting with a monolith, teams naturally design for cohesion and only introduce distribution where it provides measurable value.
Conclusion
Microservices are a powerful tool, but they are not the only tool. The monolith, especially in its modular form, remains a highly effective architecture for many applications. It reduces operational complexity, simplifies development, and allows teams to focus on delivering value rather than managing infrastructure. The next time you are tempted to decompose a system into microservices, pause. Ask yourself if the complexity is justified by the scale and the team size. Often, the answer will be no. And that is not a failure; it is a sign of pragmatic engineering.