Columna vertical de bloques oscuros apilados en capas, con una línea de luz púrpura que la divide, sobre fondo oscuro

Vertical Slice Architecture en .NET: guía práctica

Llevo casi 20 años decidiendo cómo organizar código .NET, y la pregunta que más veces me han hecho últimamente es la misma: ¿organizamos por capas técnicas o por funcionalidad? La respuesta corta es que depende, pero la respuesta útil necesita explicar de qué depende. De eso trata este post: qué es vertical slice architecture, cuándo la elegiría yo y cuándo no.

En el post anterior sobre Clean Architecture conté cómo estructuré una migración real por capas —dominio, aplicación, infraestructura— y por qué esa separación funcionó bien ahí. Este post es el reverso de esa moneda: el caso en el que organizar por capas técnicas es, en realidad, el problema, no la solución.

Qué es Vertical Slice Architecture

Vertical slice architecture organiza el código por funcionalidad, no por capa técnica. En vez de tener una carpeta Controllers, otra Services y otra Repositories que crecen juntas para cada caso de uso nuevo, cada «slice» agrupa todo lo que necesita una funcionalidad concreta: su petición de entrada, su lógica y su acceso a datos, todo junto, en un mismo sitio.

La idea no es nueva ni mía. La popularizó Jimmy Bogard, el mismo autor de MediatR, precisamente porque estaba cansado de ver proyectos donde para entender «qué hace crear un pedido» había que saltar entre cinco carpetas distintas. Cada slice se convierte en su propia unidad, con sus propias dependencias, sin obligar a tocar una capa compartida cada vez que cambia un caso de uso.

Por capas o por funcionalidad: la pregunta que de verdad importa

La arquitectura por capas técnicas parte de una premisa razonable: separar responsabilidades por tipo (presentación, negocio, datos) para que cada capa se pueda cambiar sin romper las otras. Funciona muy bien cuando el dominio es rico, compartido entre muchos casos de uso, y las reglas de negocio pesan más que la variación entre funcionalidades. Es el enfoque que usé en la migración de Clean Architecture: un dominio con reglas complejas que se reutilizan en muchos flujos distintos.

El problema aparece cuando el dominio es más simple pero el número de casos de uso crece rápido, y cada uno tiene su propia forma de entrada y salida. Ahí, la arquitectura por capas empieza a costar más de lo que aporta: cada funcionalidad nueva obliga a tocar cuatro o cinco archivos en cuatro o cinco carpetas distintas, casi siempre para añadir código que no comparte nada real con el resto.

Después de veinte años viendo crecer proyectos .NET organizados así, hay tres síntomas que reconozco enseguida. El primero es el cambio simple que acaba tocando cinco carpetas: un campo nuevo en un formulario que obliga a modificar el controlador, el servicio, el repositorio, el DTO y el mapeo entre todos ellos, para algo que en realidad solo afecta a un caso de uso. El segundo son los repositorios genéricos que nadie reutiliza de verdad: se crearon “por si acaso” varios casos de uso los compartían, y con el tiempo cada uno le añade un método específico hasta que el repositorio genérico ya no es genérico ni de nada. El tercero son las capas que existen porque el manual dice que deben existir, no porque resuelvan un problema real del proyecto — una capa de “servicios de aplicación” vacía de lógica propia, que solo reenvía la llamada a la capa de abajo, añadiendo indirección sin añadir valor.

Este enfoque de organizar por funcionalidad invierte la pregunta. En vez de preguntar «¿en qué capa va esto?», pregunta «¿qué necesita esta funcionalidad concreta para funcionar, de principio a fin?». La respuesta suele ser más corta y más fácil de encontrar seis meses después, cuando alguien del equipo —a veces yo mismo— vuelve a ese código sin recordar los detalles.

Cuándo elegir vertical slice architecture

Mi criterio, después de dos décadas viendo crecer proyectos .NET, es bastante simple:

  • Elijo este enfoque cuando el proyecto tiene muchos casos de uso independientes entre sí, con poca lógica de negocio compartida, y donde la velocidad de añadir una funcionalidad nueva importa más que la reutilización entre ellas.
  • Elijo capas técnicas o Clean Architecture cuando hay un dominio rico y estable que varias funcionalidades comparten de verdad, y donde proteger ese dominio de detalles de infraestructura pesa más que la rapidez de cada cambio individual.

No son opuestos irreconciliables. Es habitual empezar un proyecto organizado por funcionalidad para ir rápido en las primeras entregas, y extraer un núcleo de dominio compartido más adelante, cuando ese dominio empieza a repetirse entre distintas partes del código. Forzar la decisión “correcta” desde el primer día, antes de conocer el dominio real, sale más caro que decidir tarde con información real delante.

Un ejemplo práctico en .NET

Un ejemplo simplificado de cómo se ve una vertical slice, usando MediatR y Minimal API — no es código de un proyecto real, es una ilustración del patrón:

// Slices/Pedidos/CrearPedido.cs
public record CrearPedidoCommand(string ClienteId, List<LineaPedido> Lineas)
    : IRequest<CrearPedidoResultado>;

public record CrearPedidoResultado(Guid PedidoId, decimal Total);

public class CrearPedidoHandler : IRequestHandler<CrearPedidoCommand, CrearPedidoResultado>
{
    private readonly IPedidoRepository _repositorio;

    public CrearPedidoHandler(IPedidoRepository repositorio)
    {
        _repositorio = repositorio;
    }

    public async Task<CrearPedidoResultado> Handle(
        CrearPedidoCommand comando, CancellationToken cancellationToken)
    {
        var pedido = Pedido.Crear(comando.ClienteId, comando.Lineas);
        await _repositorio.GuardarAsync(pedido, cancellationToken);
        return new CrearPedidoResultado(pedido.Id, pedido.Total);
    }
}

// Program.cs
app.MapPost("/pedidos", async (CrearPedidoCommand comando, IMediator mediator) =>
    Results.Ok(await mediator.Send(comando)));

Todo lo que necesita esta funcionalidad —el contrato de entrada, la lógica y la persistencia— vive en un único archivo o carpeta. Si mañana cambia cómo se crea un pedido, se toca este slice y ningún otro. No hay una capa Services compartida que otras cien funcionalidades también usen y que haya que tener en cuenta antes de tocar nada.

Cómo organizo las carpetas cuando trabajo así

La estructura de carpetas suele agruparse por funcionalidad de negocio, no por tipo técnico:

/Slices
  /Pedidos
    CrearPedido.cs
    CancelarPedido.cs
    ConsultarPedido.cs
  /Clientes
    RegistrarCliente.cs
    ActualizarDatosCliente.cs

Cada archivo (o carpeta, si la funcionalidad crece) contiene el comando o consulta, su handler y, si hace falta, su propio modelo de datos de entrada y salida. No hay una carpeta Models compartida donde todo el mundo añade clases sin relación clara entre sí. Cuando un desarrollador nuevo entra al equipo, encontrar «todo lo que pasa cuando se cancela un pedido» es cuestión de abrir un archivo, no de perseguir referencias por media docena de carpetas.

Esto no es solo la teoría del patrón. En un proyecto real que construí así, cada operación —cada Command o cada Query— era su propio slice autónomo con su propia carpeta, sin que ninguno conociera los detalles internos de los demás. Cuando llegaba una funcionalidad nueva, añadirla era crear una carpeta más, no abrir una estructura compartida que ya usaban otras veinte funcionalidades y en la que un cambio mal medido podía afectar a algo que no tenía nada que ver.

Cómo pruebo cada slice de forma aislada

Uno de los beneficios que menos se cuenta de organizar por funcionalidad es lo que simplifica el testing. Cada handler tiene una entrada y una salida claras, y sus dependencias suelen ser pocas y explícitas —normalmente un repositorio o un servicio externo—, así que escribir un test de un caso de uso completo no exige levantar medio sistema ni simular cinco capas intermedias. El test se parece mucho al propio código de producción: mismo comando de entrada, mismo resultado esperado.

Qué pasa con las reglas transversales

Cualquier proyecto real tiene piezas que no pertenecen a un único caso de uso: autenticación, registro de auditoría, validación de entrada común, manejo de errores. Organizar por funcionalidad no significa duplicar esas piezas en cada slice. Se resuelven igual que en cualquier otra arquitectura: con middleware, con comportamientos de pipeline en MediatR, o con clases de utilidad compartidas que cada slice consume sin necesidad de reimplementarlas.

La diferencia está en qué se comparte y qué no. Comparto infraestructura transversal —lo que es igual para todos los casos de uso por definición— y dejo que la lógica de negocio de cada funcionalidad viva en su propio slice, sin forzarla a pasar por una capa de servicio genérica que en la práctica solo sirve a esa única funcionalidad. Esa distinción es la que marca si el resultado es un proyecto ordenado o una colección de carpetas con nombres distintos que reinventan el mismo problema.

Errores comunes al adoptar este enfoque

El error más habitual que he visto —y que he cometido yo mismo al principio— es duplicar sin pensar. Copiar y pegar un handler para crear el siguiente es rápido, pero si dos o tres slices empiezan a compartir una regla de negocio real, y no solo código parecido por casualidad, hay que sacar esa regla a un sitio común antes de que cambie en un slice y se quede desactualizada en los demás. La disciplina no es “nunca compartas nada”: es distinguir cuándo el parecido es casualidad y cuándo es una regla que de verdad debe ser una sola.

El segundo error es aplicar esta forma de organizar el código a un dominio que no lo pide, solo porque está de moda. Si el proyecto ya tiene un núcleo de negocio compartido y estable entre módulos, forzar la separación por funcionalidad añade fricción donde antes no la había.

Vertical Slice Architecture vs Clean Architecture: mi criterio

No elijo una arquitectura por moda ni por lo último que he leído. Las dos son herramientas válidas y las he usado en producción, en momentos distintos y por motivos distintos. Clean Architecture protege un dominio rico y compartido de los detalles de infraestructura; vertical slice architecture prioriza que cada funcionalidad se pueda entender, cambiar y probar sin tener que cargar con el resto del sistema en la cabeza.

Si tu proyecto tiene un dominio con reglas de negocio complejas que se repiten en muchos flujos, probablemente Clean Architecture te sirva mejor, como me sirvió a mí en el caso que conté en el post anterior. Si tu proyecto es una colección de casos de uso independientes que crecen rápido y no comparten apenas lógica, este enfoque probablemente te ahorre más tiempo del que cuesta aprenderlo.

¿Estás decidiendo cómo estructurar un proyecto .NET nuevo? En Desarrollo .NET a medida tomo esta decisión de arquitectura antes de escribir la primera línea de código, no después.

Fuente externa citada: Vertical Slice Architecture, por Jimmy Bogard — el artículo que popularizó el patrón, y la referencia que cito cuando alguien me pregunta de dónde viene el nombre.