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
- Qué es un monolito y por qué importa
- Monolito clásico (big ball of mud)
- Monolito modular
- Comparativa: clásico vs modular vs microservicios
- Cuándo tiene sentido un monolito
- Mitos sobre el monolito
- Errores comunes y consejos
- Ejercicios
- Conclusión
- 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.
- 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
@RestControllerparsea 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
DataSourcehacen 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.
- 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
endLas 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
ServicioPedidosImplesclasssinpublic(package-private): otros módulos no pueden instanciarla ni acoplarse a ella; solo conocen la interfazServicioPedidos. - 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.
- 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.
- 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.
- 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. |
- 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,repositoriesglobales fomentan el acoplamiento. Prefierepedidos,clientes,facturasy, 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.
- 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
└── internalCada 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
PoliticaDescuentosdentro 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.
- 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
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
