Por qué dividir un modular monolith por capa (controllers, services, repositories) sale caro a largo plazo — y cómo trazar fronteras por capacidad de negocio, con Common Closure Principle, ley de Conway y medición vía git history.
Dividir un modular monolith por capa (controllers, services, repositories) parece prolijo y sale caro. La frontera que sobrevive es por capacidad de negocio: lo que cambia junto, vive junto. El nombre formal es Common Closure Principle, de Robert C. Martin (2002). La ley de Conway, de 1968, dice que el diseño del sistema refleja el diseño del equipo. Juntá los dos y git ya te cuenta dónde están las fronteras reales de tu código hoy.
La mayoría de los equipos que conocí, cuando el monolito empieza a doler, saltan directo a microservicios. Yo ya pasé por eso tres veces. En las tres, el problema no era el monolito. Era el dibujo de las fronteras dentro de él.
Modular monolith es la palabra de moda. Pero la palabra esconde lo que de verdad importa: dónde dibujás la línea entre una cosa y otra. Esa decisión sobrevive a cualquier elección de framework.
¿Qué es un modular monolith?
Un modular monolith es una aplicación que sigue siendo deployada como un único proceso, pero internamente está cortada en módulos con fronteras explícitas. Cada módulo tiene su propio dominio, su código y a veces su propio schema de base. Mantiene la simplicidad operativa del monolito clásico, sin el acoplamiento entre features.
La diferencia práctica frente a las otras formas comunes:
| Característica |
Monolito clásico |
Modular monolith |
Microservicios |
| Unidad de deploy |
1 proceso |
1 proceso |
N procesos |
| Frontera entre módulos |
Convención |
Paquete o módulo explícito |
Red (HTTP, gRPC) |
| Base de datos |
1 schema compartido |
1 base, schemas por módulo |
1 base por servicio |
| Comunicación |
Llamada directa en memoria |
Llamada directa vía interfaces |
RPC síncrono o eventos asíncronos |
| Costo operativo |
Bajo |
Bajo |
Alto (orquestación, observabilidad, fallas distribuidas) |
| Refactorizar una frontera |
Doloroso (acoplamiento implícito) |
Mecánico (mover una carpeta) |
Caro (migración de datos, deploy coordinado) |
| Tamaño de equipo adecuado |
1 a 3 equipos |
3 a 10 equipos |
10+ equipos con dominios independientes |
La mayor parte de lo que sigue es sobre la columna del medio: cómo dibujar la frontera interna que hace que un modular monolith se sostenga a sí mismo por años.
La pregunta que nadie hace temprano
Cuando un equipo decide crear un módulo nuevo, la conversación casi siempre arranca por sustantivos. "Hagamos el módulo de productos." "Esto va en el módulo de pedidos." "Falta el módulo de usuarios."
El sustantivo es fácil. El sustantivo entra en el diagrama. El problema es que el sustantivo no dice nada sobre lo que cambia junto.
La pregunta útil es otra: cuando algo cambie en ese dominio, ¿quién tiene que cambiar con él? Si la respuesta es "tres módulos diferentes en tres PRs separados", la frontera está mal. Dibujaste una línea donde no hay ninguna.
Esto tiene nombre (y lo tiene desde 2002)
Cuando finalmente paré a estudiar por qué este patrón funcionaba, descubrí que Robert C. Martin ya le había dado nombre en 2002, en Agile Software Development: Principles, Patterns, and Practices. Lo llamó Common Closure Principle: las clases que cambian juntas, por los mismos motivos, deben vivir juntas. Las que cambian por motivos distintos, deben vivir separadas.
En otras palabras: la frontera buena no es la que se ve linda en el diagrama. Es la que minimiza la cantidad de módulos tocados por un cambio de negocio.
Vale sumarle la ley de Conway, de 1968: el diseño de un sistema refleja el diseño de la organización que lo produce. Si tu equipo está dividido por capacidad (pagos, catálogo, búsqueda), el código sostenible también se divide por capacidad. Cuando el equipo cambia, el diseño tiene que cambiar con él. Cuando el diseño no acompaña, duele.
Nada de esos dos textos es nuevo. Lo nuevo es cuántos equipos en 2025 todavía organizan código como si esas dos ideas no existieran.
Un caso que se me quedó
Hace algunos años heredé un sistema de checkout cortado por capa. El árbol raíz se veía así:
src/
controllers/
CheckoutController.ts
PaymentController.ts
OrderController.ts
ShippingController.ts
services/
CheckoutService.ts
PaymentService.ts
OrderService.ts
ShippingService.ts
repositories/
CheckoutRepository.ts
PaymentRepository.ts
OrderRepository.ts
dtos/
...
entities/
...
Cada carpeta con unos cien archivos por feature. Lindo en la descripción. Doloroso en la práctica.
Agregar un campo en el formulario de pago exigía tocar ocho archivos repartidos en siete carpetas: un DTO, una entity, un repository (y migration), un service, un controller, dos tests, y un archivo de validación central. El equipo ya había automatizado parte con snippets del editor. Pensaban que era productividad. Era anestesia.
Cuando reorganicé por lo que cambia junto, quedó así:
src/
checkout/
http/
domain/
persistence/
tests/
payment/
http/
domain/
persistence/
tests/
shipping/
http/
domain/
persistence/
tests/
shared/
logging/
db/
http-kernel/
Cada carpeta de arriba es una capacidad de negocio. Adentro, todo lo que hace que esa capacidad exista. Agregar el mismo campo ahora tocaba dos archivos, en el mismo directorio. El code review dejó de requerir dos pestañas abiertas.
No inventé nada. Sólo dejé de organizar por lo que se parece y empecé a organizar por lo que cambia junto.
Cómo medir antes de refactorizar
Lo que falta en la mayoría de las charlas sobre boundaries es que la frontera correcta se puede descubrir con dato, no sólo con intuición. Ya tenés el dato: está en git.
La heurística más simple: archivos que cambian en el mismo commit, repetidamente, durante meses, son cohesivos. Git lo sabe. Sólo tenés que preguntar.
# Archivos más cambiados en los últimos 6 meses,
# ordenados por cantidad de commits que los tocaron.
git log --since=6.months.ago --name-only --pretty=format: \
| awk 'NF' \
| sort | uniq -c | sort -rn | head -30
Eso ya te dice cuáles archivos están calientes. Para encontrar pares acoplados (la parte que importa), lo honesto es ir commit por commit. Algo así sirve:
# Lista, para cada commit de los últimos 6 meses, los archivos
# tocados. Sirve para hacer grep manual de pares sospechosos.
git log --since=6.months.ago --name-only --pretty=format:'---%H' \
> /tmp/commits.txt
Para algo más serio vale code-maat, de Adam Tornhill. Es la herramienta detrás de Your Code as a Crime Scene y Software Design X-Rays. Calcula temporal coupling: la probabilidad de que dos archivos cambien juntos. Sale en CSV, fácil de rankear. Pares con 80%+ de acoplamiento temporal entre módulos distintos son la evidencia más barata que vas a tener de frontera mal puesta.
# Uso simplificado (luego de generar el log en su formato).
maat -l logfile.log -c git2 -a coupling \
| sort -t, -k3 -rn | head -20
Yo lo corro antes de cualquier reescritura grande. En media hora, el repositorio te cuenta dónde están las fronteras reales del sistema. Refactorizar se vuelve honesto: estás arreglando algo que la historia ya probó que estaba roto.
Por qué separar por capa parece correcto
Entiendo la tentación. Separar por capa es simétrico, enseña arquitectura limpia de un vistazo, y te da la sensación reconfortante de que cada archivo tiene su lugar.
El problema es que la simetría es visual, no de comportamiento. Te llevás un diagrama lindo y perdés cohesión. Un equipo que separa por capa va, con el tiempo, a abrir PRs cada vez más grandes, porque todo cambio real cruza fronteras horizontales.
Y un PR grande no es sólo lento. Un PR grande es donde el bug se esconde.
Tres preguntas antes de crear un módulo nuevo
Cuando dibujo o reviso una frontera, paso por tres preguntas. En este orden.
¿Quién es el dueño de esa decisión? Si más de un equipo decide sobre el mismo módulo, o la frontera está en el lugar equivocado, o el equipo lo está. Uno de los dos va a cambiar.
¿Quién necesita enterarse cuando esto cambie? Si la respuesta es "todo el canal de ingeniería", el módulo está a cargo de algo demasiado central para su tamaño. Es hora de explicitar el contrato. O de partirlo en dos cosas con nombres honestos.
Si borro este módulo mañana, ¿qué se rompe? Respuesta sana: una capacidad clara desaparece, con síntomas previsibles. Respuesta mala: dieciséis lugares dejan de funcionar por motivos distintos. Eso es acoplamiento escondido. La frontera existe en el diagrama, no en el código.
Cuándo la regla no aplica
No funciona en todo. Utilidades puras (formato de fechas, parsing de moneda, helpers de strings) no tienen dominio propio. Forzarlas dentro de un módulo es agregar indirección.
El código de plataforma también escapa. Logger, cliente de base de datos, instrumentación de tracing. Ese tipo de cosas cruza todos los módulos por diseño. Tratarlo como módulo de negocio sólo estorba. Es lo que vive en shared/ en el ejemplo de arriba, y cuanto menos aparezca ahí, mejor.
Las integraciones de borde también merecen cuidado. Un webhook de Stripe no es "el módulo Stripe". Es parte del módulo que recibe el pago, traducido en el borde. La frontera está entre lo que es tu modelo y lo que es el contrato de ellos.
La prueba de los seis meses
La parte más útil de este enfoque sólo aparece después. Seis meses, un año. Abrís el repo para tocar algo que no veías hace tiempo, y la pregunta es simple: ¿todavía duele?
Si duele, la frontera se corrió. Probablemente porque el negocio cambió y nadie actualizó el diseño. Pasa. Refactorizar una frontera es mantenimiento normal, no una falla del pasado. Volvé a correr git log y mirá dónde se movió el acoplamiento.
Si no duele, hiciste lo correcto. No porque adivinaste el futuro, sino porque dibujaste por lo que cambia junto, y eso siguió siendo cierto después de que el equipo olvidó por qué se había hecho así.
Modular monolith no es un patrón. Es la consecuencia de tomarse esa pregunta en serio desde el primer commit.
Preguntas frecuentes
¿Modular monolith es lo mismo que microservicios?
No. Microservicios corren en procesos separados que se comunican por red. Modular monolith corre en un único proceso. El parecido es sólo interno: ambos fuerzan fronteras explícitas entre capacidades. El costo operativo es completamente distinto.
¿Cuándo conviene migrar de modular monolith a microservicios?
Cuando una capacidad tiene necesidad real de escalar de forma independiente, ciclo de deploy distinto, o equipo dedicado. No migres por moda. Cada microservicio carga costo permanente de observabilidad, deploys y fallas en cascada. Ese costo sólo compensa cuando la capacidad ya no entra en el monolito.
¿Cómo empiezo a reorganizar un monolito existente por capacidad?
Empezá midiendo. Corré git log sobre los últimos seis meses y rankeá archivos que cambian juntos con frecuencia. Esos pares apuntan a capacidades implícitas que la arquitectura actual está cortando por la mitad. Usá code-maat si querés ir más allá del git log manual. Reorganizá un módulo a la vez, en PRs menores a 500 líneas.
¿Modular monolith funciona con cualquier lenguaje?
Sí. No es un patrón de framework, es un patrón de organización. Los ejemplos de este artículo usan TypeScript, pero la misma división por capacidad funciona en Go, Java, .NET, Python o Ruby. Lo que cambia es la herramienta que enforza la frontera (paquetes, módulos, namespaces), no la regla.
Para seguir leyendo
Estos son los textos que cito cuando alguien quiere profundizar. Todos sostienen, desde algún ángulo, la tesis de arriba.
- Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002), capítulos sobre Package Cohesion Principles. Reformulado en Clean Architecture (2017), capítulo "Component Cohesion". El nombre formal de lo que defiende este artículo.
- Melvin Conway, How Do Committees Invent? (1968). El paper original de la ley de Conway, todavía sorprendentemente corto.
- Martin Fowler, MonolithFirst (2015). Por qué empezar por microservicios casi siempre es prematuro.
- Adam Tornhill, Your Code as a Crime Scene (2024, 2ª ed.). Todo lo que se puede descubrir leyendo git history en vez de adivinando.
- Kirsten Westeinde, Deconstructing the Monolith (Shopify Engineering, 2019). Cómo uno de los monolitos más grandes en producción fue reorganizado por capacidad, no por capa.
- DHH, The Majestic Monolith (2016). La defensa corta y honesta del monolito bien diseñado.
- Kamil Grzybek, Modular Monolith: A Primer (2019). Serie técnica en .NET, con ideas agnósticas de stack.
Enable JavaScript for the full experience.