Arquitectura

Monolito modular vs Vertical Slice: diferencias y cuándo elegir

Qué diferencia a un monolito modular de Vertical Slice, qué problema resuelve cada uno, cuándo conviene cada enfoque y cómo combinarlos en Java y Spring Boot.

Publicado el 13 min de lectura Nivel intermedio Por
  • Java
  • Spring Boot
  • Arquitectura
  • Monolito Modular
  • Vertical Slice
  • DDD
  • Modularidad
  • Acoplamiento
Ilustración del artículo: Monolito modular vs Vertical Slice: diferencias y cuándo elegir

Capítulo 01. El problema no es tener un monolito

Un sistema no se vuelve difícil de mantener por desplegarse como una sola aplicación. Se vuelve difícil cuando nadie es capaz de decir dónde empieza y dónde acaba una funcionalidad.

La escena se repite con una regularidad sospechosa. Una plataforma de comercio electrónico arrancó con cuatro entidades y dos desarrolladores. Tres años después hay catorce personas, el dominio cubre clientes, catálogo, pedidos y facturación, y un cambio en el cálculo de impuestos de una factura obliga a tocar siete ficheros repartidos entre cuatro paquetes. Ninguno de esos paquetes se llama facturación.

Cuando eso pasa, la propuesta que aparece en la reunión suele ser la misma: dividir en microservicios. Es una respuesta cara a una pregunta que todavía no se ha formulado bien. Antes de repartir el sistema en artefactos independientes hay una decisión más barata, más reversible y casi siempre más rentable: ¿alrededor de qué organizamos el código?

Conviene separar dos cosas que se mezclan constantemente. Una es cómo se despliega el sistema: un artefacto o varios. Otra es cómo se organiza el código dentro de ese artefacto. El monolito modular y Vertical Slice son respuestas a la segunda pregunta. Las dos conviven perfectamente con un único despliegue.

Tres formas de cortar el mismo sistema

El mismo código fuente admite al menos tres ejes de corte principales, y cada uno decide dónde cae un cambio.

  • Monolito tradicional. El corte principal es la capa técnica: controller, service, repository, model. Todo el sistema comparte esos cuatro cajones, sin importar a qué parte del negocio pertenezca cada clase.
  • Monolito modular. El corte principal es el módulo de negocio: clientes, catálogo, pedidos, facturación. Cada módulo tiene un límite explícito y publica un contrato; lo demás es interno y nadie de fuera lo toca.
  • Organización Vertical Slice. El corte principal es el caso de uso: crear pedido, cancelar pedido, consultar pedido. Cada operación reúne en un mismo sitio todo lo que necesita para ejecutarse.

Que un eje sea el principal no elimina a los demás. Dentro de un módulo sigue habiendo capas, y dentro de un slice también. Lo que cambia es qué criterio manda en el primer nivel de la estructura de paquetes.

Un cambio de negocio sobre una estructura por capas técnicas

Diagrama de cuatro capas técnicas —controladores, servicios, repositorios y modelo— donde las clases resaltadas muestran que un único cambio de facturación obliga a tocar las cuatro capas y dos áreas de negocio distintas.

Controladores
CustomerController
OrderController
InvoiceController
Servicios
CustomerService
OrderService 900 líneas, 11 casos de uso
InvoiceService
Repositorios
CustomerRepository
OrderRepository
InvoiceRepository
Modelo
Customer
Order
Invoice
TaxRule
Las celdas resaltadas son los ficheros que toca un solo cambio: añadir una regla de impuestos a la emisión de facturas. El cambio es uno; los directorios, cuatro. Y la clase que más crece, OrderService, la comparten casos de uso que no tienen nada que ver entre sí.

El problema del diagrama anterior no es que existan capas. Las capas separan responsabilidades reales y eso sigue siendo útil. El problema es que, cuando la capa es el criterio principal, el código que cambia junto vive separado y el código que no tiene relación acaba compartiendo la misma clase. Eso es acoplamiento accidental: no viene del dominio, viene del sitio donde hemos puesto los ficheros.

El resto del artículo responde a cuatro preguntas concretas: qué resuelve cada enfoque, en qué se diferencian de verdad, cuándo conviene cada uno y cómo se combinan. Porque se combinan, y esa es la parte que más veces se pierde en la discusión.

Capítulo 02. Qué es un monolito modular

Un único despliegue dividido en módulos de negocio con límites que una prueba puede hacer cumplir. La diferencia con tener paquetes bien nombrados es que aquí los límites se defienden.

Un monolito modular es una aplicación que se despliega como un solo artefacto y cuyo corte principal es el dominio. En lugar de cuatro capas compartidas por todo el sistema, hay módulos de negocio: clientes, catálogo, pedidos y facturación en el ejemplo que venimos usando.

Qué pertenece a cada módulo

A un módulo pertenece todo lo necesario para sostener sus reglas: sus entidades, su lógica, su persistencia y sus puntos de entrada. El criterio para decidir dónde va algo no es qué tablas toca, sino quién decide cuándo cambia esa regla. Si la política de reintento de un cobro la fija el área financiera, vive en facturación aunque consulte datos de pedidos.

Es el mismo razonamiento que hay detrás de un contexto delimitado en Domain-Driven Design, aplicado al primer nivel de paquetes. Si el reparto en módulos no se parece al reparto de responsabilidades de la organización, los límites se van a romper solos.

El límite no es el paquete: es el contrato

Cada módulo expone una superficie deliberadamente pequeña —una o dos interfaces publicadas, y eventos de dominio cuando la relación es asíncrona— y mantiene todo lo demás como interno. Facturación no importa OrderEntity; recibe un evento OrderPlaced o llama a una operación del contrato de pedidos que devuelve un objeto propio de ese contrato.

Cuando ese límite no existe, pasa algo concreto y bastante difícil de revertir: una consulta JPQL lanzada desde facturación sobre la tabla de pedidos convierte el esquema interno de pedidos en API pública sin que el equipo de pedidos se entere. El día que renombren una columna romperán un código que no conocen, y lo descubrirán en producción.

Un despliegue, cuatro módulos, contratos explícitos

Diagrama por capas de un monolito modular: una capa de entrada única, una capa con los cuatro módulos de negocio, una capa de contratos publicados que es la única vía de comunicación entre módulos, y una capa de infraestructura compartida sin reglas de negocio.

Entrada HTTP, mensajería, tareas programadas
API de la aplicación Un único artefacto desplegable
Módulos Corte por dominio
Clientes
Catálogo
Pedidos
Facturación
Contratos publicados La única superficie visible desde fuera
OrderQuery Interfaz publicada
OrderPlaced Evento de dominio
CustomerQuery Interfaz publicada
Infraestructura compartida Transversal, sin reglas de negocio
Seguridad
Configuración
Observabilidad
Acceso a datos
Lo que hace modular a esta estructura es la tercera capa. Si un módulo puede saltársela y alcanzar las clases internas de otro, el diagrama sigue siendo cierto sobre el papel y falso sobre el código.

Dependencias, ciclos y cómo se hacen cumplir

Un monolito modular necesita una regla de dependencia escrita: pedidos puede depender del contrato de catálogo, catálogo no puede depender de pedidos. En cuanto aparece un ciclo, los dos módulos implicados dejan de poder razonarse por separado, dejan de poder probarse por separado y dejan de poder extraerse por separado. Un ciclo no es un defecto de estilo: es la pérdida del único beneficio por el que se paga la estructura.

La regla que funciona en producción es sencilla de enunciar y exigente de aplicar: el límite tiene que romper el build. Una convención documentada en la wiki aguanta tres meses; una prueba de arquitectura con ArchUnit o la verificación de módulos de Spring Modulith aguanta mientras exista el proyecto. También sirven los módulos de Maven o el sistema de módulos de la plataforma Java, a cambio de una estructura de proyecto más rígida.

Una base de datos compartida no rompe la modularidad

Compartir una instancia de base de datos es perfectamente compatible con un monolito modular. Lo que importa no es cuántas bases de datos hay, sino la propiedad de los datos: cada tabla pertenece a un módulo y solo ese módulo la escribe y la consulta directamente.

A partir de ahí hay un abanico de opciones con costes distintos. Un esquema por módulo dentro de la misma instancia da un límite visible y permisos separados, manteniendo las transacciones locales. Un esquema único con convención de prefijo es más barato de montar y más fácil de incumplir. Una base de datos por módulo facilita una extracción futura, pero elimina los JOIN entre módulos y obliga a consistencia eventual desde el primer día, muchas veces antes de que el sistema lo necesite.

Capítulo 03. Qué es Vertical Slice Architecture

Organizar el código alrededor de lo que el sistema hace, no de la capa técnica a la que pertenece cada clase. Un caso de uso, un paquete, y dentro todo lo que necesita para ejecutarse.

Volvamos al OrderService de novecientas líneas de la primera sección. Añadir «cancelar pedido» a una estructura por capas obliga a tocar cuatro directorios y a meter un método más en una clase que ya comparten otros diez casos de uso sin ninguna relación entre sí. A partir de cierto tamaño, cada cambio exige entender código que no tiene nada que ver con el cambio.

Vertical Slice invierte el criterio: el corte principal es el caso de uso. Un slice agrupa el punto de entrada, la lógica de la operación, sus validaciones, el acceso a datos que necesita, los modelos propios de esa operación y sus pruebas.

Todo lo que toca «crear pedido», en un solo paquete

Diagrama de flujo: CreateOrderController → CreateOrderRequest → CreateOrderHandler → Order → OrderRepository → CreateOrderTest

  1. CreateOrderController POST /orders · solo esta operación
  2. CreateOrderRequest Modelo de entrada propio del slice
  3. CreateOrderHandler La lógica de este caso de uso y de ningún otro
  4. Order Dominio del módulo: compartido, no duplicado
  5. OrderRepository La estrategia de acceso que conviene a esta operación
  6. CreateOrderTest De la petición HTTP a la fila en base de datos
El slice contiene la operación completa, pero no todo lo que usa. El agregado Order pertenece al dominio del módulo y lo comparten los demás casos de uso: es la línea que separa agrupar de duplicar.

Qué no significa Vertical Slice

Cuatro aclaraciones que ahorran discusiones. Ninguna es un matiz menor.

  • No es una arquitectura de despliegue. Un slice no es un servicio, no se despliega por separado y no escala por separado. Describe cómo se agrupa el código fuente dentro de una aplicación.
  • No exige una base de datos por funcionalidad. Lo normal es que todos los slices compartan la misma. Lo que cambia es cómo se accede a ella, no cuántas hay.
  • No obliga a duplicar la lógica. Las invariantes del dominio siguen viviendo en el modelo, en un único sitio. Lo que deja de compartirse es el flujo de la operación, no las reglas.
  • No implica que los slices sean independientes entre sí. Comparten dominio, infraestructura y, muy a menudo, transacción.

Dónde está la ganancia real

La ventaja que más se nota en un sistema en producción no es estética: es que cada operación puede usar la estrategia de acceso a datos que le conviene. Crear un pedido necesita el agregado completo, con sus invariantes y su transacción. Listar los pedidos de un cliente para una pantalla necesita cinco columnas, y cargar el agregado entero para descartar el noventa por ciento es un coste que se paga en cada petición.

Con un repositorio genérico compartido por todos los casos de uso, esa diferencia se resuelve añadiendo métodos y parámetros hasta que el repositorio sirve mal a todos. Con slices, la escritura usa JPA y la lectura proyecta una consulta SQL directamente sobre un DTO, sin que ninguna de las dos decisiones contamine a la otra. Es el mismo razonamiento que sostiene la separación entre comandos y consultas, aplicado sin necesidad de adoptar CQRS como tal.

Cuándo empeora las cosas

Vertical Slice reduce el acoplamiento accidental y a cambio abre la puerta a la duplicación. Si cada slice reimplementa el cálculo de impuestos, el sistema tiene ahora cinco versiones de una regla de negocio y ninguna garantía de que coincidan. Ese fallo no se ve en una revisión de código: se ve en una conciliación contable.

El otro riesgo es la deriva. Veinte slices escritos por seis personas a lo largo de dos años, sin una referencia común, producen veinte formas distintas de validar una entrada y de devolver un error. La estructura no impone cohesión por sí sola.

Capítulo 04. Las diferencias que importan

No compiten en el mismo plano. Uno decide dónde están las fronteras del negocio; el otro, cómo se organiza el trabajo dentro de una frontera.

Casi todas las comparativas entre estos dos enfoques fallan por el mismo sitio: los presentan como opciones excluyentes y acaban mezclando organización del código con decisiones de infraestructura. La tabla siguiente se limita a lo primero, que es lo único en lo que realmente se diferencian.

Monolito modular y Vertical Slice, criterio a criterio
CriterioMonolito modularVertical Slice
Unidad principal de organizaciónEl módulo de negocio: clientes, pedidos, facturación.El caso de uso: crear pedido, cancelar pedido, consultar pedido.
Dónde está el límiteEntre módulos, en un contrato publicado y una regla de dependencia verificable.Entre slices, por convención de paquete. El compilador no lo impone.
Dónde vive la lógica de negocioEn el modelo del módulo, compartido por todos sus casos de uso.En el handler del slice, salvo las invariantes, que siguen en el agregado.
Acoplamiento entre funcionalidadesAlto dentro del módulo y bajo entre módulos, pero siempre explícito.Bajo entre slices. El riesgo se desplaza del acoplamiento a la divergencia.
Reutilización de lógicaPor diseño: el módulo es la unidad que se reutiliza.Deliberadamente contenida: se reutiliza el dominio, no el flujo de la operación.
Impacto de un cambioAcotado al módulo mientras el contrato no cambie. Si cambia, afecta a sus consumidores.Acotado al slice mientras la regla no esté compartida. Si lo está, hay que buscar copias.
PruebasPrueba de módulo contra su contrato, más pruebas de arquitectura que vigilan los límites.Prueba por caso de uso, de la petición de entrada a la persistencia.
Coste de mantenimientoGobernar contratos y dependencias. Cuesta cuando hay muchos módulos pequeños.Vigilar la duplicación y la deriva de estilo. Cuesta cuando hay muchos slices y poca referencia común.
Evolución del sistemaUn módulo puede extraerse más adelante si aparece un motivo. No es obligatorio que ocurra.Un slice se reescribe o se borra entero sin arrastrar al resto.
Todos los criterios son de organización del código fuente. El número de artefactos desplegados, la estrategia de escalado y el número de bases de datos no aparecen porque no los determina ninguno de los dos enfoques.

La fila que mejor resume la diferencia es la segunda. En un monolito modular el límite es un artefacto del proyecto: existe una interfaz publicada y existe una prueba que falla si alguien la rodea. En Vertical Slice el límite es una convención: nada impide que un handler importe clases de otro slice, y lo que lo evita es la disciplina del equipo y la revisión de código.

Eso no convierte a uno en superior. Significa que resuelven problemas distintos y que la pregunta «¿cuál elijo?» suele estar mal planteada, como veremos en la sección 6.

Capítulo 05. Cuándo encaja cada enfoque

Las recomendaciones dependen del dominio, del tamaño del equipo, de la frecuencia de cambio y de la capacidad real de sostener una regla. Fuera de ese contexto, ninguna lista de ventajas significa nada.

Cuándo suele encajar un monolito modular

  • El dominio tiene límites reconocibles. Si puedes nombrar cuatro o cinco áreas de negocio y señalar quién decide en cada una, los módulos ya existen en la organización y el código solo tiene que reflejarlos.
  • Necesitas imponer reglas entre áreas. Cuando hay partes del sistema que no deben tocarse desde cualquier sitio —precios, límites de crédito, datos personales—, el contrato publicado es el mecanismo que lo hace verificable en el build en vez de confiarlo a la revisión.
  • Varios equipos comparten un despliegue. El módulo da propiedad sin exigir separar artefactos: cada equipo manda en lo suyo y negocia solo el contrato.
  • Hay lógica de negocio que atraviesa casos de uso. Si veinte operaciones dependen de las mismas invariantes de pedido, tener un modelo de dominio compartido dentro de un límite es justo lo que evita veinte copias.
  • Podrías necesitar extraer un módulo. Un módulo con contrato y sin ciclos es extraíble el día que aparezca un motivo real: un ritmo de despliegue incompatible, un requisito de aislamiento, una carga que no cabe. Que sea posible no significa que vaya a ocurrir, ni que deba.

Cuándo suele encajar Vertical Slice

  • La aplicación gira alrededor de operaciones, no de áreas. Un back-office, un motor de trámites o una API de integración son conjuntos de casos de uso heterogéneos dentro de un mismo dominio.
  • Las operaciones se parecen poco entre sí. Cuando una escritura transaccional y un listado de lectura intensiva comparten el mismo repositorio, el repositorio acaba sirviendo mal a las dos.
  • El equipo necesita localizar un cambio rápido. Un paquete por caso de uso reduce el tiempo de «dónde está esto» prácticamente a cero, y eso importa mucho cuando hay rotación o incorporaciones frecuentes.
  • Las funcionalidades evolucionan a ritmos distintos. Si una operación cambia cada semana y otra lleva dos años intacta, compartir servicio obliga a arrastrar la segunda en cada despliegue de la primera.
  • Organizar por capas está dispersando los cambios. El síntoma medible es el número de ficheros que toca una historia de usuario media. Si crece sin que crezca la funcionalidad, el eje de corte está trabajando en contra.

Cuándo cada uno resulta excesivo

El monolito modular sobra cuando el dominio no tiene fronteras claras todavía. Dibujar módulos sobre un dominio que aún no se entiende produce límites en el sitio equivocado, y un límite mal puesto es más caro de mover que la ausencia de límite. En un sistema pequeño con un solo equipo, el coste de gobernar contratos no lo paga nadie.

Vertical Slice sobra cuando la aplicación tiene pocas operaciones y se parecen mucho entre sí. Un CRUD sobre seis entidades organizado en treinta paquetes de un fichero cada uno no ha reducido ninguna complejidad: ha añadido ceremonia. También sobra cuando el equipo no tiene todavía criterio compartido sobre qué es dominio y qué es flujo, porque es justo la decisión que el enfoque obliga a tomar en cada slice.

Por dónde empezar, según el sistema que tienes delante

  1. ¿Puedes nombrar tres o más áreas de negocio con responsables distintos?

  2. ¿Alguna de esas áreas tiene reglas que no deberían tocarse desde el resto del sistema?

    • No especialmente Pasa a la pregunta 03
  3. ¿Las operaciones del sistema son heterogéneas entre sí: escrituras transaccionales junto a lecturas intensivas, ritmos de cambio distintos?

    • No, son parecidas Pasa a la pregunta 04
  4. ¿El sistema supera las veinte o treinta operaciones y lo mantiene más de un equipo?

    • Sí Monolito modular con slices dentro de cada módulo. A ese tamaño hacen falta los dos ejes: la frontera de negocio y la localización del cambio.
    • No Capas, y vuelve a esta pregunta dentro de seis meses. Estructurar un dominio que todavía no se entiende fija los límites en el sitio equivocado.
El árbol ordena el criterio, no lo sustituye. La respuesta a la última pregunta cambia con el tiempo, y revisarla es parte del trabajo de arquitectura.

Capítulo 06. Monolito modular con slices dentro

Los dos enfoques cortan por ejes distintos, así que componerlos no es un término medio entre ambos: es usar cada uno para lo que sirve.

El módulo responde a dónde está la frontera del negocio. El slice responde a dónde cae un cambio funcional. Son preguntas independientes, y por eso la estructura que las combina no tiene nada de híbrida: el primer nivel de paquetes es el dominio y el segundo es el caso de uso.

ESTRUCTURA
src/main/java/com/tienda/
├── customers/                  ← módulo: frontera de negocio
│   ├── CustomerQuery.java          contrato publicado
│   ├── domain/                     modelo del módulo
│   └── usecases/
│       ├── createcustomer/         ← slice
│       ├── getcustomer/
│       └── updatecustomer/
├── orders/
│   ├── OrderQuery.java
│   ├── OrderPlaced.java            evento de dominio
│   ├── domain/
│   └── usecases/
│       ├── createorder/
│       ├── cancelorder/
│       └── getorder/
├── billing/
│   ├── domain/
│   └── usecases/
│       ├── issueinvoice/
│       └── getinvoice/
└── shared/                     ← infraestructura, sin reglas de negocio
    ├── security/
    └── persistence/

Es un esquema, no una plantilla. Los nombres concretos, la profundidad y la decisión de agrupar los casos de uso bajo usecases/ o colgarlos directamente del módulo dependen del proyecto. Lo que sí debe conservarse es el orden de los dos ejes: primero el módulo, después la operación.

Dos ejes, dos tipos de límite

Diagrama por capas donde cada capa es un módulo de negocio —clientes, pedidos, facturación— y las celdas de cada capa son los casos de uso organizados como slices dentro de ese módulo. Una última capa recoge la infraestructura compartida sin reglas de negocio.

Módulo Clientes Límite verificable
create-customer
get-customer
update-customer
Módulo Pedidos Límite verificable
create-order
cancel-order
get-order
Módulo Facturación Límite verificable
issue-invoice
get-invoice
Compartido Sin reglas de negocio
Seguridad
Persistencia
Observabilidad
Entre capas, el límite es un contrato que una prueba hace cumplir. Dentro de cada capa, el límite entre slices es una convención. Son dos niveles de rigor distintos, y está bien que lo sean.

Qué se gana

  • Las fronteras de negocio son explícitas y verificables. La regla «ningún slice puede importar clases internas de otro módulo» es una prueba de arquitectura de cinco líneas.
  • Un cambio funcional cae en un sitio. Modificar la cancelación de pedidos toca orders/usecases/cancelorder/ y, si cambia una invariante, orders/domain/. Nada más.
  • La duplicación queda acotada. El dominio compartido vive en el módulo, así que la tentación de copiar una regla se topa con un modelo que ya está ahí y a mano.
  • El borrado es limpio. Retirar una funcionalidad es borrar un paquete y su prueba, sin ir buscando métodos huérfanos por tres servicios.

Qué cuesta

Más convenciones que explicar y defender. Un desarrollador que entra tiene que entender dos niveles de agrupación y la regla de dependencia entre módulos antes de escribir la primera línea. Eso es documentación que hay que escribir y mantener.

Y una decisión que hay que tomar en cada caso de uso: qué pertenece al dominio del módulo y qué es propio del slice. Tomarla mal hacia un lado produce duplicación de reglas; tomarla mal hacia el otro produce un modelo de dominio que crece con cada operación hasta volver al OrderService del principio, solo que con otro nombre.

Capítulo 07. El mismo módulo, de dos maneras

Una funcionalidad de creación de pedidos organizada por capas dentro del módulo y organizada por slices. La diferencia no está en los nombres de las carpetas: está en qué tienes que abrir para cambiar algo.

Capas dentro del módulo

Cuatro directorios compartidos por los seis casos de uso del módulo. Añadir una operación significa tocar los cuatro y ampliar tres clases que ya usan los demás.

Es una estructura perfectamente válida mientras el módulo sea pequeño. El problema aparece cuando OrderService pasa de once métodos a treinta.

Slices dentro del módulo

Un paquete por operación, con su entrada, su lógica, sus modelos y su prueba. El dominio del módulo queda fuera de los slices, compartido por todos.

Añadir una operación es crear un paquete. Retirarla es borrarlo. Ninguna de las dos cosas toca a las demás.

ANTES · POR CAPAS
orders/
├── controller/
│   └── OrderController.java        6 endpoints
├── service/
│   └── OrderService.java           11 métodos públicos
├── repository/
│   └── OrderRepository.java        23 métodos de consulta
└── model/
    ├── Order.java
    └── OrderDto.java
DESPUÉS · POR SLICES
orders/
├── OrderQuery.java                 contrato publicado
├── domain/
│   ├── Order.java                  invariantes del agregado
│   └── OrderRepository.java        puerto del módulo
└── usecases/
    ├── createorder/
    │   ├── CreateOrderController.java
    │   ├── CreateOrderHandler.java
    │   ├── CreateOrderRequest.java
    │   ├── CreateOrderResponse.java
    │   └── CreateOrderTest.java
    └── cancelorder/
        ├── CancelOrderController.java
        ├── CancelOrderHandler.java
        └── CancelOrderTest.java

Fíjate en dos detalles que no son cosméticos. domain/ está fuera de usecases/: ahí viven las invariantes del pedido y las comparten todos los slices, que es lo que impide que la organización por operaciones degenere en copias de las reglas. Y OrderQuery está en la raíz del módulo porque es la única clase que los demás módulos pueden ver.

El handler del caso de uso

JAVA
package com.tienda.orders.usecases.createorder;

import com.tienda.catalog.CatalogQuery;
import com.tienda.orders.domain.Order;
import com.tienda.orders.domain.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
class CreateOrderHandler {

    private final OrderRepository orders;
    private final CatalogQuery catalog;

    CreateOrderHandler(OrderRepository orders, CatalogQuery catalog) {
        this.orders = orders;
        this.catalog = catalog;
    }

    @Transactional
    CreateOrderResponse handle(CreateOrderRequest request) {

        var items = request.lines().stream()
                .map(line -> catalog.priceOf(line.sku(), line.quantity()))
                .toList();

        var order = Order.place(request.customerId(), items);

        orders.save(order);

        return CreateOrderResponse.from(order);
    }
}

El handler es visible solo dentro de su paquete —sin public— y esa es una decisión deliberada: nadie fuera del slice debería invocarlo. Depende de dos cosas, y las dos están declaradas en el constructor: el puerto de persistencia del módulo y CatalogQuery, que es el contrato publicado por catálogo. No importa ninguna clase interna de otro módulo, y ese es exactamente el límite que la siguiente prueba verifica.

La transacción vive aquí, en el caso de uso, y no en un servicio compartido. Permite que «cancelar pedido» tenga un comportamiento transaccional distinto sin negociarlo con nadie, que es una de las razones prácticas por las que esta organización reduce fricción.

La prueba que sostiene los límites

ARCHUNIT
@AnalyzeClasses(packages = "com.tienda")
class ModuleBoundaryTest {

    @ArchTest
    static final ArchRule los_modulos_solo_se_hablan_por_contrato =
            noClasses()
                    .that().resideInAPackage("com.tienda.(*)..")
                    .should().dependOnClassesThat()
                    .resideInAPackage("com.tienda.(*).domain..")
                    .because("el dominio de un módulo es interno: fuera se usa su contrato publicado");

    @ArchTest
    static final ArchRule sin_ciclos_entre_modulos =
            slices().matching("com.tienda.(*)..")
                    .should().beFreeOfCycles();
}

Esta es la pieza que convierte un diagrama en una arquitectura. La primera regla impide que un módulo alcance el dominio interno de otro; la segunda detecta ciclos entre módulos en cuanto aparecen, no seis meses después. Si falla, falla el build, y nadie tiene que acordarse de la convención durante una revisión de código a las siete de la tarde.

Quien prefiera no escribir las reglas a mano tiene en Spring Modulith una verificación equivalente basada en la estructura de paquetes, más la documentación del modelo de módulos generada a partir del propio código. Lo importante no es la herramienta: es que exista algo que falle.

Qué cambia al añadir una funcionalidad

Añadir «devolver pedido» a la estructura por capas significa un método en el controlador, uno en el servicio, dos consultas en el repositorio y un DTO nuevo: cuatro ficheros modificados que otros casos de uso también usan, y por tanto cuatro oportunidades de romper algo ajeno.

En la estructura por slices significa un paquete nuevo, orders/usecases/returnorder/, y una modificación en Order si la devolución introduce una invariante nueva. El resto del módulo no se toca. La diferencia se nota sobre todo en la revisión: el diagrama de cambios de la pull request coincide con el alcance funcional del cambio.

Capítulo 08. Errores habituales

Los cinco que aparecen con más frecuencia en revisiones de arquitectura, con el síntoma que permite detectarlos antes de que sean caros.

Cinco errores, su síntoma y su consecuencia
ErrorCómo se reconoceQué acaba pasando
Llamar modular a una aplicación que solo tiene paquetes separadosCoges una clase interna de un módulo y la importan clases de otros dos. No hay contrato ni prueba que lo impida.Se paga el coste de la estructura sin obtener el beneficio: los cambios siguen propagándose igual que antes.
Permitir dependencias circulares entre módulosNo puedes compilar, probar ni razonar sobre un módulo sin arrastrar al otro.Los dos módulos se convierten en uno de facto. Extraer cualquiera de ellos deja de ser posible sin reescribir.
Interpretar Vertical Slice como obligación de duplicarlo todoLa misma regla de negocio aparece escrita en tres handlers, con pequeñas diferencias entre ellos.Las copias divergen. El defecto no sale en una revisión de código: sale en una conciliación de datos.
Confundir organización del código con arquitectura de despliegueAlguien afirma que los slices escalan por separado, o que el módulo obliga a una base de datos compartida.Se toman decisiones de infraestructura con argumentos que no las sostienen, normalmente hacia microservicios prematuros.
Aplicar una estructura compleja a un dominio que no la necesitaTreinta paquetes de un fichero cada uno sobre un CRUD de seis entidades y un solo equipo.La ceremonia se paga todos los días a cambio de un beneficio que a ese tamaño no existe.

Los dos primeros tienen la misma raíz: un límite que nadie hace cumplir no es un límite. Se corrigen con una prueba de arquitectura, y cuesta una tarde montarla sobre un proyecto que ya existe, aunque al principio haya que marcar excepciones para las violaciones heredadas e ir reduciéndolas.

El tercero y el quinto son de criterio y no se resuelven con herramientas. Lo que ayuda es acordar de antemano dos cosas: qué pertenece al dominio compartido del módulo, y cuál es el tamaño a partir del cual la estructura deja de ser gratuita. Escribirlo en una decisión de arquitectura evita volver a discutirlo en cada pull request.

Elige según el problema que tienes

La pregunta no es cuál de las dos arquitecturas es mejor. Es qué está fallando hoy en tu sistema.

Si el problema es que cualquier parte del código puede tocar cualquier regla de negocio, lo que necesitas son límites: el monolito modular los establece y, con una prueba de arquitectura, los sostiene.

Si el problema es que cada cambio funcional se reparte por cuatro directorios y obliga a entender código ajeno al cambio, lo que necesitas es cohesión por caso de uso: Vertical Slice la da.

Y si tienes los dos problemas a la vez, que es lo habitual en un sistema con varios años y varios equipos, no hay que elegir. Los módulos marcan las fronteras del negocio; los slices organizan el trabajo dentro de ellas. Son ejes distintos del mismo código.

Lo que ninguno de los dos hace es sustituir a la decisión. Una estructura no mantiene sola un sistema: lo que lo mantiene es que las reglas de dependencia estén escritas, que algo las verifique en cada build y que el equipo entienda por qué están ahí. Sin eso, cualquiera de las dos organizaciones degenera en la anterior en un par de años.

Si vas a tocar la estructura de un sistema que ya está en producción, empieza por lo barato y reversible: dibuja los módulos que ya existen de hecho en el código, escribe una prueba de arquitectura que falle con las dependencias que no deberían existir, y mira cuántas falla. Ese número dice más sobre tu arquitectura que cualquier diagrama, y es también el mejor punto de partida para decidir qué organización necesitas de verdad.

Preguntas frecuentes

Las cinco dudas que aparecen siempre al plantear esta decisión en un equipo. Cada respuesta es autocontenida: no hace falta haber leído el artículo.

¿Qué diferencia hay entre un monolito modular y Vertical Slice?

Cortan el mismo código por ejes distintos. Un monolito modular divide el sistema en módulos de negocio —clientes, pedidos, catálogo, facturación— y pone el límite entre ellos: cada módulo publica un contrato y el resto solo puede usar ese contrato. Vertical Slice divide el código por caso de uso —crear pedido, cancelar pedido, consultar pedido— y agrupa en un mismo sitio el punto de entrada, la lógica, las validaciones, el acceso a datos y las pruebas de esa operación concreta. El monolito modular responde a dónde están las fronteras del dominio; Vertical Slice responde a cómo se organiza el trabajo dentro de una frontera. Ninguno de los dos es una arquitectura de despliegue: los dos describen organización de código dentro de una aplicación que puede seguir siendo un único artefacto.

¿Se pueden combinar un monolito modular y Vertical Slice?

Sí, y es una combinación habitual porque los dos ejes son independientes. La aplicación se divide primero en módulos de negocio con contratos y reglas de dependencia explícitas, y dentro de cada módulo los casos de uso se organizan como slices: un paquete por operación con todo lo que esa operación necesita. El módulo aporta la frontera que se puede hacer cumplir con una prueba de arquitectura; el slice aporta que un cambio funcional caiga en un solo sitio. El coste es real: más convenciones que explicar, una decisión inicial sobre qué es dominio compartido del módulo y qué es propio del slice, y vigilancia para que la duplicación no alcance a las reglas de negocio. En una aplicación con un dominio pequeño, un único módulo bien organizado suele ser suficiente.

¿Vertical Slice sustituye a la arquitectura por capas?

No la sustituye: cambia el eje principal de corte. Dentro de un slice siguen existiendo las mismas responsabilidades de siempre —entrada HTTP, lógica de aplicación, persistencia, modelo—, pero conviven en el mismo paquete en lugar de estar repartidas por cuatro directorios compartidos con el resto del sistema. Las capas no desaparecen, dejan de ser la unidad de organización. Y hay partes donde seguir organizando por capa tiene todo el sentido: la infraestructura transversal, la seguridad, la configuración o el modelo de dominio compartido no son un caso de uso y no ganan nada partiéndose por operación.

¿Qué enfoque conviene para una aplicación Spring Boot?

Depende de la forma del dominio, no de la tecnología. Si la aplicación cubre varias áreas de negocio reconocibles, con reglas que cambian por motivos distintos y posiblemente equipos distintos, el monolito modular aporta más: límites explícitos, contratos entre módulos y la opción de extraer uno más adelante sin rehacerlo. Si la aplicación gira alrededor de un conjunto de operaciones heterogéneas dentro de un mismo dominio —y el síntoma típico es un servicio de varios cientos de líneas compartido por casos de uso que no tienen relación entre sí—, organizar por slices reduce el acoplamiento accidental. Spring Boot soporta ambas formas sin ninguna extensión: paquetes, inyección por constructor y Spring Data bastan. Spring Modulith y ArchUnit solo añaden la parte que el compilador no cubre, que es verificar que los límites se respetan.

¿Vertical Slice implica tener una base de datos por funcionalidad?

No. Es el malentendido más extendido y conviene descartarlo pronto. Vertical Slice describe cómo se agrupa el código fuente; no dice nada sobre cuántas bases de datos hay, ni sobre cuántos artefactos se despliegan, ni sobre cómo se escala el sistema. Lo habitual es que todos los slices de una aplicación compartan la misma base de datos. Lo que sí cambia es cómo se accede a ella: cada slice puede usar la estrategia que le conviene —un agregado JPA para una operación de escritura, una consulta SQL proyectada directamente a un DTO para un listado— en lugar de forzar todas las operaciones a pasar por el mismo repositorio genérico. Separar bases de datos es una decisión independiente, con su propio coste en transacciones y consistencia, y hay que justificarla por sí misma.

Referencias

Fuentes primarias, con indicación de qué respalda cada una y de qué afirmaciones del artículo son hechos documentados, cuáles análisis y cuáles criterio profesional.

Hecho, análisis e interpretación
AfirmaciónNaturalezaBase
Vertical Slice describe organización del código fuente, no despliegue ni número de bases de datos.HechoDefinición original de Jimmy Bogard, que plantea el enfoque frente a la organización por capas técnicas.
Las dependencias entre módulos y los ciclos se pueden verificar automáticamente en el build.HechoDocumentación de ArchUnit y de Spring Modulith, que ofrecen reglas de corte por paquete y detección de ciclos.
Organizar por capas técnicas dispersa los cambios funcionales entre directorios.AnálisisConsecuencia del esquema de capas de presentación, dominio y datos descrito por Martin Fowler, cuando se aplica como eje principal de corte.
Los dos enfoques se combinan porque cortan por ejes independientes.InterpretaciónCriterio profesional del autor, coherente con que uno defina fronteras de dominio y el otro agrupación de casos de uso.
Un límite que no rompe el build deja de respetarse con el tiempo.InterpretaciónObservación recurrente en revisiones de arquitectura sobre plataformas Java con varios equipos.
El artículo no atribuye a ninguna fuente datos de productividad, tiempos de entrega ni porcentajes de reducción de defectos, porque no existe evidencia pública que los sostenga para esta decisión concreta.

Fuentes

Continúa en este blog

  • Arquitectura Hexagonal desde cero Cómo aislar el dominio de un módulo con puertos y adaptadores, que es la estructura interna que mejor acompaña a los contratos de la sección 2.
  • Observabilidad en Microservicios Qué hace falta tener instrumentado antes de extraer un módulo y convertirlo en un servicio independiente.
  • Arquitectura Java Decisiones estructurales en plataformas Java enterprise, más allá de la organización de paquetes.
  • Domain-Driven Design Cómo se identifican las fronteras de negocio que después se convierten en módulos.
  • Auditoría técnica El servicio que aplica a una plataforma concreta el diagnóstico con el que cierra la conclusión.
Fotografía de Javier García Pérez, Arquitecto de Software Java freelance
SOBRE EL AUTOR

Javier García Pérez

Arquitecto de Software Java freelance · Madrid, España

Más de 15 años diseñando y modernizando plataformas Java críticas en banca, retail, seguros, aerolíneas y administración pública, con proyectos para BBVA, Iberia, Carrefour, Tendam, Ocaso y el Govern de les Illes Balears.

Trabajo a diario con Spring Boot, Quarkus, arquitectura hexagonal, Domain-Driven Design, microservicios event-driven sobre Kafka, AWS, Kubernetes, observabilidad con OpenTelemetry e integración de IA generativa con Spring AI y arquitecturas RAG. Escribo sobre lo que aplico en producción, no sobre teoría.

¿Cuántas de las dependencias de tu código existen porque alguien las decidió, y cuántas porque nadie las impidió?

Es la diferencia entre una arquitectura y una estructura de carpetas. Una auditoría técnica deja el mapa real de dependencias entre módulos, los ciclos que impiden extraer o probar nada por separado, y un orden de corrección por impacto sobre la velocidad de cambio.