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

  1. What a monolith is and why it matters
  2. Classic monolith (big ball of mud)
  3. Modular monolith
  4. Comparison: classic vs modular vs microservices
  5. When a monolith makes sense
  6. Myths about the monolith
  7. Common mistakes and tips
  8. Exercises
  9. Conclusion

  1. 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.

  1. 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 @RestController parses 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 DataSource make 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.

  1. 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
    end

The 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 ServicioPedidosImpl class is class without public (package-private): other modules cannot instantiate it or couple to it; they only know the ServicioPedidos interface.
  • 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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, repositories packages encourage coupling. Prefer pedidos, clientes, facturas and, 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.

  1. 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
      └── internal

Each 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 PoliticaDescuentos within 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.

  1. 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

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved