Hay que cambiar cómo se cancela un pedido. Abres controllers, luego services, después repositories y terminas buscando un estado entre cuarenta modelos. Todo está ordenado por tipo de archivo. La cancelación, en cambio, está repartida por el edificio.

Un corte vertical agrupa el código alrededor de una funcionalidad. La pregunta de entrada pasa a ser «¿qué parte de la tienda hace esto?». Dentro de esa parte seguimos separando las responsabilidades que merezcan un límite propio.

En arquitectura en capas distinguimos presentación, coordinación, reglas y acceso a datos. Ahora cambiamos el criterio con el que encontramos el código. Un presupuesto puede tener esas responsabilidades dentro de quotes, sin obligarnos a recorrer todas las carpetas horizontales de la aplicación.

Cambiar el primer criterio de agrupación

Una organización horizontal reúne controladores con controladores y repositorios con repositorios. Es fácil encontrar todos los controladores; puede costar más reunir las piezas de una operación.

controllers/  QuoteController, CancellationController
services/     QuoteService, CancellationService
repositories/ Catalog, OrderRepository
models/       Product, Order

Organizar por funcionalidad cambia esa primera clasificación:

quotes/         QuoteController, QuoteService
cancellations/  CancellationController, CancellationService
catalog/        Catalog, Product
orders/         OrderRepository, Order

El árbol es orientativo. Un corte puede corresponder a un caso de uso, como cancelar, o agruparse dentro de una capacidad mayor, como pedidos. No existe una carpeta obligatoria por cada botón de la interfaz.

La diferencia se aprecia al ampliar una cancelación: presentación y coordinación se encuentran juntas. Si también cambia una regla compartida del pedido, habrá que tocar orders. La organización vertical mejora la localización; no promete encerrar cualquier cambio en un solo archivo.

Es otra forma de buscar la cohesión que presentamos en los fundamentos: aproximar las piezas que colaboran para un propósito reconocible. La autonomía se pierde si un corte importa detalles internos de todos los demás, aunque el árbol de carpetas quede estupendo en la captura.

Un caso de uso que se entiende de un vistazo

Supongamos una regla deliberadamente pequeña: un pedido se puede cancelar mientras no esté enviado. Este ejemplo completo usa Java 17 o posterior y vive en un solo archivo SlicesExample.java. Las clases aparecen juntas para leerlas; los comentarios indican la agrupación conceptual.

enum OrderStatus { PAID, SHIPPED, CANCELLED }

// orders
final class Order {
    private OrderStatus status;

    Order(OrderStatus status) {
        this.status = java.util.Objects.requireNonNull(status);
    }

    void cancel() {
        if (status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("A shipped order cannot be cancelled.");
        }
        status = OrderStatus.CANCELLED;
    }

    OrderStatus status() { return status; }
}

// cancellations
record CancelOrder(Order order) {
    OrderStatus execute() {
        order.cancel();
        return order.status();
    }
}

// orderstatus
record ReadOrderStatus(Order order) {
    OrderStatus execute() {
        return order.status();
    }
}

final class SlicesExample {
    public static void main(String[] args) {
        var order = new Order(OrderStatus.PAID);
        new CancelOrder(order).execute();
        System.out.println(new ReadOrderStatus(order).execute());
    }
}

La salida es CANCELLED. Repetir la cancelación conserva ese estado; cancelar uno enviado falla. La regla vive en Order, porque debe protegerse sea cual sea la entrada. CancelOrder expresa la acción de cancelar y ReadOrderStatus expresa una consulta. Podemos localizar cada recorrido sin convertir la consulta en una dependencia de la cancelación.

El ejemplo omite almacenamiento, autenticación y reembolsos. Compartimos la misma instancia para mostrar el resultado y no abrir una discusión sobre repositorios. En una aplicación, el caso de uso cargaría y guardaría el pedido; cancelar un pedido pagado necesitaría además una política explícita de devolución. Estos tipos enseñan organización, no implementan una compra completa.

Qué se comparte y qué permanece en cada corte

Los dos casos utilizan Order. Eso tiene sentido: comparten el significado del estado del pedido. No sería útil que uno declarase CANCELLED y otro mantuviera su propia interpretación de un número mágico.

Compartir ese modelo tampoco obliga a compartir todas las respuestas. El resultado de una consulta puede contener fecha y seguimiento; la cancelación puede comunicar una aceptación y una referencia de operación. Que hoy ambos devuelvan un enum es una simplificación del ejemplo.

CancelOrder y ReadOrderStatus utilizan Order, pero ninguno depende del otro.

Las flechas indican dependencias de código. Los casos colaboran a través de un concepto compartido, sin invocarse mutuamente. Si ReadOrderStatus tuviera que ejecutar parte de CancelOrder para conocer el estado, habría una mezcla de responsabilidades que las carpetas no arreglarían.

Al descubrir código parecido en dos cortes conviene preguntar si representa la misma regla. Un formateador puede compartirse; dos políticas comerciales que hoy coinciden pueden evolucionar por separado. Extraer todo a common puede sustituir varias carpetas comprensibles por una colección de dependencias bastante menos comprensible.

La distinción es la misma que explicamos al hablar de DRY y la duplicación de conocimiento. Si cambia el significado de «pedido cancelable», queremos corregir una regla. Si cambian dos representaciones de pantalla por motivos distintos, mantenerlas separadas puede ser razonable.

Vertical no significa meterlo todo en un método

Una funcionalidad puede contener presentación, aplicación, dominio y adaptadores. Si una cancelación reúne reglas complejas, podemos organizarlas dentro de orders/cancel/ sin mezclar SQL, HTML y autorización en una sola función.

También cabe una funcionalidad sencilla con un manejador y una consulta. La estructura interna depende de la complejidad real. La propuesta de Vertical Slice Architecture de Jimmy Bogard pone el foco en los cambios de cada funcionalidad y en evitar que una estructura uniforme obligue a resolver todos los casos igual.

Cuando esa funcionalidad se comunica con un transportista o una base de datos, los puertos y adaptadores de la arquitectura hexagonal pueden ayudar a aislar esa conversación. Es una combinación posible; organizar por cortes no exige que todas las operaciones tengan el mismo número de interfaces.

En Java, una organización real necesita paquetes y visibilidad coherentes. Un nombre de carpeta no impide importar un detalle de otra. Es posible limitar lo público a la entrada del módulo y comprobar las dependencias con herramientas; el ejemplo en un único archivo no demuestra ese aislamiento de compilación.

Elegir el tamaño de la frontera

Si cada método recibe una carpeta, pronto necesitarás un mapa para encontrar la carpeta correcta. Si toda la tienda se llama shop, el corte será tan ancho como la aplicación. Buscamos agrupaciones que correspondan a decisiones reconocibles y que puedan evolucionar con cierta autonomía.

Podemos comenzar con catálogo, pedidos y pagos. Dentro de pedidos distinguimos crear, consultar y cancelar cuando ayuda a orientarse. Una política de cancelación que cambia junto a las transiciones del pedido puede permanecer cerca del modelo, aunque la entrada web tenga su propia carpeta.

Los cambios reales dan información: si dos cortes necesitan modificarse siempre a la vez, quizá comparten una regla mal situada o una frontera poco útil. Si colaboran por una operación estable, tocar dos módulos puede ser perfectamente razonable. Contar carpetas afectadas no basta para diagnosticar el diseño.

Hacer una mudanza pequeña

Para probar este enfoque en un proyecto existente, elige una operación y reúne sus piezas conservando el comportamiento. Revisa importaciones y visibilidad después del movimiento. Añade una segunda operación para comprobar qué conviene compartir y qué no.

Es una refactorización: cambia la estructura, mientras las expectativas observables se conservan. Si aprovechamos la mudanza para permitir cancelar pedidos enviados, también estaremos cambiando una regla y necesitaremos verificar esa decisión por separado.

No hace falta reorganizar toda la tienda en una tarde. Una buena prueba es pedir a otra persona que encuentre dónde se decide si un pedido enviado se puede cancelar. Si llega a Order.cancel() sin recorrer media aplicación, la nueva dirección ya sirve para algo.