La arquitectura monolítica es el punto de partida histórico y conceptual de casi cualquier sistema de software. Consiste en construir y desplegar una aplicación como una única unidad cohesionada: todo el código (interfaz de usuario, lógica de negocio y acceso a datos) vive en el mismo proyecto, se compila junto y se ejecuta en un solo proceso. A pesar de la moda de los microservicios, comprender el monolito es imprescindible, porque sigue siendo la opción más sensata para la mayoría de proyectos que empiezan, y porque muchos de los "antipatrones" que sufren los sistemas distribuidos nacen de no haber entendido bien cómo organizar un buen monolito. En esta lección distinguiremos el monolito clásico del monolito modular, veremos cuándo tiene sentido cada uno y desmontaremos varios mitos extendidos.

Contenido

  1. Qué es un monolito y por qué importa
  2. Monolito clásico (big ball of mud)
  3. Monolito modular
  4. Comparativa: clásico vs modular vs microservicios
  5. Cuándo tiene sentido un monolito
  6. Mitos sobre el monolito
  7. Errores comunes y consejos
  8. Ejercicios
  9. Conclusión

  1. Qué es un monolito y por qué importa

Un monolito es una aplicación que se empaqueta y despliega como una sola unidad ejecutable. En el mundo Java suele ser un único .jar o .war; en otros entornos, un único contenedor o proceso.

Características definitorias:

  • Un solo artefacto de despliegue. Toda la aplicación se libera a la vez.
  • Un solo proceso en ejecución. Las llamadas entre módulos son llamadas a método en memoria, no peticiones de red.
  • Una base de código compartida. Todos los equipos trabajan sobre el mismo repositorio (habitualmente).
  • Normalmente, una sola base de datos. Aunque no es obligatorio.
graph TD
    Usuario[Usuario / Navegador] --> Mono
    subgraph Mono[Proceso único - Monolito]
        UI[Capa de Presentación]
        BL[Lógica de Negocio]
        DA[Acceso a Datos]
        UI --> BL --> DA
    end
    DA --> DB[(Base de Datos)]

El diagrama muestra que, aunque internamente exista una separación lógica en capas, todo se ejecuta dentro del mismo proceso y se despliega junto. Esa es la diferencia esencial con un sistema distribuido.

  1. Monolito clásico (big ball of mud)

El monolito clásico es la forma más común y, a menudo, la peor entendida. No es malo por ser monolito, sino por cómo suele degenerar: cuando no se imponen fronteras internas claras, el código termina en lo que Brian Foote y Joseph Yoder bautizaron como "big ball of mud" (gran bola de barro).

Síntomas de un monolito degradado:

  • Cualquier clase puede llamar a cualquier otra; no hay módulos reales.
  • La lógica de negocio se mezcla con el acceso a datos y con la presentación.
  • Un cambio pequeño obliga a tocar decenas de archivos por todo el proyecto.
  • El conocimiento del sistema vive solo en la cabeza de unas pocas personas.
// Antipatrón típico del monolito clásico: todo mezclado en un controlador
@RestController
public class PedidoController {

    @Autowired private DataSource dataSource; // acceso a datos directo

    @PostMapping("/pedidos")
    public String crearPedido(@RequestBody Map<String,Object> body) throws Exception {
        // 1) Lógica de presentación (parseo) mezclada
        String cliente = (String) body.get("cliente");
        double total = (Double) body.get("total");

        // 2) Reglas de negocio embebidas en el controlador
        if (total > 1000) total = total * 0.95; // descuento "mágico"

        // 3) SQL crudo en la misma clase
        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"; // sin manejo de errores serio
    }
}

Analicemos por qué este fragmento es problemático:

  • Mezcla de responsabilidades: el @RestController parsea la petición (presentación), aplica un descuento (negocio) y ejecuta SQL (persistencia). Tres responsabilidades distintas en un solo método.
  • Regla de negocio enterrada: el descuento del 5% está escondido en el controlador. Si otro endpoint crea pedidos, esa regla se duplicará o se olvidará.
  • Acoplamiento a la infraestructura: el SQL crudo y el DataSource hacen que la lógica sea imposible de probar sin una base de datos real.

Importante: ser monolito no obliga a escribir así. Este es el monolito mal hecho. Veamos cómo evitarlo.

  1. Monolito modular

El monolito modular conserva todas las ventajas operativas del monolito (un solo despliegue, sin red entre módulos) pero impone fronteras internas estrictas entre módulos de negocio. Cada módulo expone una API pública y oculta sus detalles internos; los demás módulos solo pueden comunicarse a través de esa API.

graph TD
    subgraph Monolito Modular
        direction LR
        Pedidos[Módulo Pedidos\nAPI pública]
        Clientes[Módulo Clientes\nAPI pública]
        Facturas[Módulo Facturas\nAPI pública]
        Pedidos --> Clientes
        Facturas --> Pedidos
    end

Las claves de un monolito modular:

  • Encapsulación por módulo: cada módulo tiene su propio paquete raíz y oculta sus clases internas.
  • Comunicación por contratos: los módulos se hablan a través de interfaces, no accediendo a clases internas.
  • Cohesión por dominio: se agrupa por capacidad de negocio (Pedidos, Clientes, Facturación), no por capa técnica.
// Módulo Pedidos: API pública (lo único que otros módulos ven)
package com.fiatc.pedidos.api;

public interface ServicioPedidos {
    Long crearPedido(NuevoPedido datos);
}

// Implementación interna (package-private, invisible fuera del módulo)
package com.fiatc.pedidos.internal;

import com.fiatc.pedidos.api.ServicioPedidos;

class ServicioPedidosImpl implements ServicioPedidos {

    private final RepositorioPedidos repositorio;
    private final PoliticaDescuentos descuentos; // regla aislada y testeable

    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);
    }
}

Qué ha mejorado respecto al ejemplo anterior:

  • La clase ServicioPedidosImpl es class sin public (package-private): otros módulos no pueden instanciarla ni acoplarse a ella; solo conocen la interfaz ServicioPedidos.
  • La regla de descuento vive en PoliticaDescuentos, una clase única y reutilizable que se puede probar de forma aislada.
  • El acceso a datos está detrás de RepositorioPedidos, por lo que la lógica de negocio no sabe nada de SQL.

Herramientas como Spring Modulith o el sistema de módulos de Java (JPMS) ayudan a verificar automáticamente que estas fronteras no se rompan.

  1. Comparativa: clásico vs modular vs microservicios

Criterio Monolito clásico Monolito modular Microservicios
Fronteras internas Difusas o inexistentes Estrictas (en código) Físicas (red)
Despliegue Único Único Múltiple e independiente
Comunicación entre módulos Llamada en memoria Llamada en memoria vía API Red (HTTP/mensajería)
Coste operativo Bajo Bajo Alto (orquestación, observabilidad)
Escalado Todo o nada Todo o nada Granular por servicio
Complejidad inicial Baja Media Muy alta
Riesgo de "bola de barro" Alto Bajo Se traslada al sistema distribuido
Refactor de fronteras Costoso (acoplamiento) Barato (es código) Muy costoso (contratos de red)

La lección clave de la tabla: el monolito modular suele ser el "punto dulce" para muchos equipos. Captura disciplina arquitectónica sin pagar el coste operativo de lo distribuido, y deja la puerta abierta a extraer microservicios más adelante si un módulo lo necesita.

  1. Cuándo tiene sentido un monolito

Un monolito (preferiblemente modular) es una buena elección cuando:

  • El equipo es pequeño o mediano (hasta unas pocas decenas de personas).
  • El dominio aún no está claro: las fronteras de los módulos cambiarán mucho, y refactorizar dentro de un monolito es barato.
  • No hay necesidades de escalado dispares: todas las partes del sistema reciben una carga similar.
  • Se quiere maximizar la velocidad de entrega inicial y minimizar la complejidad operativa.
  • Las transacciones cruzan varios módulos: dentro de un proceso se resuelven con una transacción ACID local; en lo distribuido requieren patrones complejos (sagas).

Regla práctica muy citada (Martin Fowler, MonolithFirst): empieza con un monolito y extrae servicios solo cuando el dolor lo justifique.

  1. Mitos sobre el monolito

Mito Realidad
"Monolito = código espagueti" El desorden viene de la falta de fronteras, no del despliegue único. Un monolito modular puede estar mejor organizado que un mar de microservicios.
"Los monolitos no escalan" Escalan horizontalmente desplegando varias instancias detrás de un balanceador. Lo que no escala es el escalado granular por componente.
"Hay que empezar con microservicios para no migrar luego" Empezar distribuido con un dominio inmaduro suele producir fronteras erróneas muy caras de mover.
"Un monolito implica una sola tecnología/lenguaje" Cierto en el proceso, pero rara vez es un problema real frente a las ventajas.
"Desplegar un monolito siempre es lento" Con CI/CD modernos, compilar y desplegar un monolito bien estructurado es perfectamente ágil.

  1. Errores Comunes y Consejos

  • No imponer fronteras desde el día uno. Aunque sea un monolito, organiza por dominio y oculta los detalles internos. Es lo único que evita la bola de barro.
  • Organizar por capa técnica en lugar de por dominio. Paquetes controllers, services, repositories globales fomentan el acoplamiento. Prefiere pedidos, clientes, facturas y, dentro, las capas.
  • Confundir "modular" con "muchos archivos". Modular significa encapsulación real (visibilidad package-private, APIs explícitas), no solo dividir en paquetes.
  • Saltar a microservicios por moda. Si tu monolito duele, primero pregúntate si el problema es de modularidad, no de despliegue.
  • Consejo: usa herramientas como Spring Modulith o ArchUnit para verificar automáticamente que nadie cruza las fronteras de los módulos.

  1. Ejercicios

Ejercicio 1. Tienes un monolito con paquetes globales controllers, services y repositories. Propón una reorganización por dominio para una aplicación de seguros con las capacidades: Pólizas, Siniestros y Clientes. Describe la estructura de paquetes.

Ejercicio 2. Dado el PedidoController del apartado 2, identifica las tres responsabilidades mezcladas y propón a qué clase/colaborador debería ir cada una.

Ejercicio 3. Razona si los siguientes escenarios justifican microservicios o un monolito modular: (a) Startup de 4 desarrolladores con dominio incierto. (b) Un componente concreto recibe 100 veces más carga que el resto y debe escalar por separado.

Soluciones

Solución 1. Estructura orientada a dominio:

com.fiatc.seguros
 ├── polizas
 │    ├── api          (interfaces públicas: ServicioPolizas)
 │    └── internal     (impl, repositorio, entidades - package-private)
 ├── siniestros
 │    ├── api
 │    └── internal
 └── clientes
      ├── api
      └── internal

Cada dominio contiene sus propias capas; los dominios se comunican solo a través de los paquetes api.

Solución 2.

  • Parseo de la petición HTTP → responsabilidad de presentación; debe quedarse en el controlador, pero delegando de inmediato.
  • Descuento del 5% → regla de negocio; debe ir a una clase como PoliticaDescuentos dentro del módulo Pedidos.
  • INSERT en BD → persistencia; debe ir a un RepositorioPedidos.

El controlador debería limitarse a recibir el DTO y llamar a ServicioPedidos.crearPedido(...).

Solución 3. (a) Monolito modular. Dominio incierto y equipo pequeño: las fronteras cambiarán y el refactor debe ser barato. (b) Candidato a extraer un microservicio para ese componente concreto, si el escalado granular justifica el coste operativo. Aun así, conviene haberlo aislado primero como módulo dentro del monolito.

  1. Conclusión

El monolito no es el villano de la arquitectura moderna; el villano es la falta de fronteras internas. Un monolito modular ofrece simplicidad operativa, transacciones locales y refactor barato, y es la mejor opción de partida para la inmensa mayoría de proyectos. Hemos visto la diferencia con el monolito clásico, cuándo elegirlo y los mitos que conviene desmontar. En la siguiente lección profundizaremos en cómo estructurar internamente cualquier aplicación —monolítica o no— mediante la Arquitectura en Capas (N-Tier), donde estudiaremos las capas de presentación, negocio y persistencia, y antipatrones clásicos como el de la "capa sumidero".

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados