Monolithic architecture is the historical and conceptual starting point for almost any software system. It consists of building and deploying an application as a single cohesive unit: all the code (user interface, business logic, and data access) lives in the same project, is compiled together, and runs in a single process. Despite the microservices trend, understanding the monolith is essential, because it remains the most sensible option for the majority of projects that are just starting, and because many of the "antipatterns" that distributed systems suffer from arise from not having properly understood how to organize a good monolith. In this lesson we will distinguish the classic monolith from the modular monolith, see when each one makes sense, and debunk several widespread myths.
Contents
- What a monolith is and why it matters
- Classic monolith (big ball of mud)
- Modular monolith
- Comparison: classic vs modular vs microservices
- When a monolith makes sense
- Myths about the monolith
- Common mistakes and tips
- Exercises
- Conclusion
- What a monolith is and why it matters
A monolith is an application that is packaged and deployed as a single executable unit. In the Java world it is usually a single .jar or .war; in other environments, a single container or process.
Defining characteristics:
- A single deployment artifact. The entire application is released at once.
- A single running process. Calls between modules are in-memory method calls, not network requests.
- A shared code base. All teams work on the same repository (usually).
- Normally, a single database. Although it is not mandatory.
graph TD
Usuario[User / Browser] --> Mono
subgraph Mono[Single process - Monolith]
UI[Presentation Layer]
BL[Business Logic]
DA[Data Access]
UI --> BL --> DA
end
DA --> DB[(Database)]The diagram shows that, even though internally there is a logical separation into layers, everything runs within the same process and is deployed together. That is the essential difference from a distributed system.
- Classic monolith (big ball of mud)
The classic monolith is the most common form and, often, the most misunderstood. It is not bad because it is a monolith, but because of how it tends to degenerate: when clear internal boundaries are not enforced, the code ends up as what Brian Foote and Joseph Yoder dubbed a "big ball of mud."
Symptoms of a degraded monolith:
- Any class can call any other; there are no real modules.
- Business logic gets mixed with data access and presentation.
- A small change forces you to touch dozens of files all over the project.
- Knowledge of the system lives only in the heads of a few people.
// Typical classic-monolith antipattern: everything mixed in a controller
@RestController
public class PedidoController {
@Autowired private DataSource dataSource; // direct data access
@PostMapping("/pedidos")
public String crearPedido(@RequestBody Map<String,Object> body) throws Exception {
// 1) Presentation logic (parsing) mixed in
String cliente = (String) body.get("cliente");
double total = (Double) body.get("total");
// 2) Business rules embedded in the controller
if (total > 1000) total = total * 0.95; // "magic" discount
// 3) Raw SQL in the same class
try (var con = dataSource.getConnection()) {
var ps = con.prepareStatement(
"INSERT INTO pedidos(cliente,total) VALUES(?,?)");
ps.setString(1, cliente);
ps.setDouble(2, total);
ps.executeUpdate();
}
return "OK"; // no serious error handling
}
}Let's analyze why this snippet is problematic:
- Mixing of responsibilities: the
@RestControllerparses the request (presentation), applies a discount (business), and runs SQL (persistence). Three distinct responsibilities in a single method. - Buried business rule: the 5% discount is hidden in the controller. If another endpoint creates orders, that rule will be duplicated or forgotten.
- Coupling to infrastructure: the raw SQL and the
DataSourcemake the logic impossible to test without a real database.
Important: being a monolith does not force you to write like this. This is the badly built monolith. Let's see how to avoid it.
- Modular monolith
The modular monolith keeps all the operational advantages of the monolith (a single deployment, no network between modules) but enforces strict internal boundaries between business modules. Each module exposes a public API and hides its internal details; other modules can only communicate through that API.
graph TD
subgraph Modular Monolith
direction LR
Pedidos[Orders Module\npublic API]
Clientes[Customers Module\npublic API]
Facturas[Invoices Module\npublic API]
Pedidos --> Clientes
Facturas --> Pedidos
endThe keys to a modular monolith:
- Encapsulation per module: each module has its own root package and hides its internal classes.
- Communication through contracts: modules talk to each other through interfaces, not by accessing internal classes.
- Cohesion by domain: grouping is done by business capability (Orders, Customers, Invoicing), not by technical layer.
// Orders module: public API (the only thing other modules see)
package com.fiatc.pedidos.api;
public interface ServicioPedidos {
Long crearPedido(NuevoPedido datos);
}
// Internal implementation (package-private, invisible outside the module)
package com.fiatc.pedidos.internal;
import com.fiatc.pedidos.api.ServicioPedidos;
class ServicioPedidosImpl implements ServicioPedidos {
private final RepositorioPedidos repositorio;
private final PoliticaDescuentos descuentos; // isolated, testable rule
ServicioPedidosImpl(RepositorioPedidos r, PoliticaDescuentos d) {
this.repositorio = r;
this.descuentos = d;
}
@Override
public Long crearPedido(NuevoPedido datos) {
double total = descuentos.aplicar(datos.total());
return repositorio.guardar(datos.cliente(), total);
}
}What has improved compared to the previous example:
- The
ServicioPedidosImplclass isclasswithoutpublic(package-private): other modules cannot instantiate it or couple to it; they only know theServicioPedidosinterface. - The discount rule lives in
PoliticaDescuentos, a single, reusable class that can be tested in isolation. - Data access is behind
RepositorioPedidos, so the business logic knows nothing about SQL.
Tools such as Spring Modulith or the Java module system (JPMS) help automatically verify that these boundaries are not broken.
- Comparison: classic vs modular vs microservices
| Criterion | Classic monolith | Modular monolith | Microservices |
|---|---|---|---|
| Internal boundaries | Blurry or nonexistent | Strict (in code) | Physical (network) |
| Deployment | Single | Single | Multiple and independent |
| Communication between modules | In-memory call | In-memory call via API | Network (HTTP/messaging) |
| Operational cost | Low | Low | High (orchestration, observability) |
| Scaling | All or nothing | All or nothing | Granular per service |
| Initial complexity | Low | Medium | Very high |
| Risk of "ball of mud" | High | Low | Shifted to the distributed system |
| Refactoring boundaries | Expensive (coupling) | Cheap (it's code) | Very expensive (network contracts) |
The key lesson from the table: the modular monolith is often the "sweet spot" for many teams. It captures architectural discipline without paying the operational cost of distributed systems, and it leaves the door open to extracting microservices later if a module needs it.
- When a monolith makes sense
A monolith (preferably modular) is a good choice when:
- The team is small or medium (up to a few dozen people).
- The domain is not yet clear: module boundaries will change a lot, and refactoring within a monolith is cheap.
- There are no disparate scaling needs: all parts of the system receive a similar load.
- You want to maximize initial delivery speed and minimize operational complexity.
- Transactions span several modules: within a single process they are resolved with a local ACID transaction; in distributed systems they require complex patterns (sagas).
A widely cited rule of thumb (Martin Fowler, MonolithFirst): start with a monolith and extract services only when the pain justifies it.
- Myths about the monolith
| Myth | Reality |
|---|---|
| "Monolith = spaghetti code" | The mess comes from the lack of boundaries, not from single deployment. A modular monolith can be better organized than a sea of microservices. |
| "Monoliths don't scale" | They scale horizontally by deploying several instances behind a load balancer. What does not scale is granular scaling per component. |
| "You have to start with microservices to avoid migrating later" | Starting distributed with an immature domain usually produces wrong boundaries that are very expensive to move. |
| "A monolith implies a single technology/language" | True within the process, but rarely a real problem compared with the advantages. |
| "Deploying a monolith is always slow" | With modern CI/CD, compiling and deploying a well-structured monolith is perfectly agile. |
- Common Mistakes and Tips
- Not enforcing boundaries from day one. Even if it is a monolith, organize by domain and hide internal details. That is the only thing that prevents the ball of mud.
- Organizing by technical layer instead of by domain. Global
controllers,services,repositoriespackages encourage coupling. Preferpedidos,clientes,facturasand, inside them, the layers. - Confusing "modular" with "many files." Modular means real encapsulation (package-private visibility, explicit APIs), not just splitting into packages.
- Jumping to microservices because of fashion. If your monolith hurts, first ask yourself whether the problem is about modularity, not about deployment.
- Tip: use tools such as Spring Modulith or ArchUnit to automatically verify that no one crosses module boundaries.
- Exercises
Exercise 1. You have a monolith with global packages controllers, services, and repositories. Propose a domain-oriented reorganization for an insurance application with the capabilities: Policies, Claims, and Customers. Describe the package structure.
Exercise 2. Given the PedidoController from section 2, identify the three mixed responsibilities and propose which class/collaborator each one should go to.
Exercise 3. Reason whether the following scenarios justify microservices or a modular monolith: (a) A startup of 4 developers with an uncertain domain. (b) A specific component receives 100 times more load than the rest and must scale separately.
Solutions
Solution 1. Domain-oriented structure:
com.fiatc.seguros
├── polizas
│ ├── api (public interfaces: ServicioPolizas)
│ └── internal (impl, repository, entities - package-private)
├── siniestros
│ ├── api
│ └── internal
└── clientes
├── api
└── internalEach domain contains its own layers; domains communicate only through the api packages.
Solution 2.
- Parsing the HTTP request → presentation responsibility; it should stay in the controller, but delegating immediately.
- The 5% discount → business rule; it should go to a class such as
PoliticaDescuentoswithin the Orders module. - INSERT into the DB → persistence; it should go to a
RepositorioPedidos.
The controller should limit itself to receiving the DTO and calling ServicioPedidos.crearPedido(...).
Solution 3. (a) Modular monolith. Uncertain domain and small team: boundaries will change and refactoring must be cheap. (b) Candidate for extracting a microservice for that specific component, if granular scaling justifies the operational cost. Even so, it is advisable to have first isolated it as a module within the monolith.
- Conclusion
The monolith is not the villain of modern architecture; the villain is the lack of internal boundaries. A modular monolith offers operational simplicity, local transactions, and cheap refactoring, and it is the best starting choice for the vast majority of projects. We have seen the difference from the classic monolith, when to choose it, and the myths worth debunking. In the next lesson we will go deeper into how to internally structure any application—monolithic or not—through Layered Architecture (N-Tier), where we will study the presentation, business, and persistence layers, and classic antipatterns such as the "sinkhole layer."
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
