La web de la tienda calcula un presupuesto. Recibe una cantidad, busca el precio y devuelve el total. Después el panel de administración necesita la misma operación. Copiamos el controlador y, con él, la regla que impide presupuestar más de cinco unidades. Un mes después ampliamos el límite en una pantalla. En la otra sigue siendo cinco. La tienda tiene dos opiniones sobre el mismo producto.

La arquitectura en capas ayuda a separar responsabilidades que cambian por motivos diferentes. Vamos a aplicarla a ese presupuesto con muy poco código: interesa entender dónde vive cada decisión y cómo colaboran las piezas.

En los fundamentos de arquitectura vimos que cohesión y acoplamiento ayudan a razonar sobre el reparto del trabajo. Las capas convierten esa idea en una organización concreta: reunir lo que responde a una misma responsabilidad y limitar qué necesita conocer de las demás piezas.

Cuatro preguntas, cuatro responsabilidades

Podemos descomponer la operación en cuatro preguntas:

  • Presentación, QuoteController: ¿cómo interpreto la petición y presento el resultado?
  • Aplicación, QuoteService: ¿qué pasos hacen falta para obtener el presupuesto?
  • Dominio, Product: ¿qué cantidades se admiten y cómo se calcula el total?
  • Acceso a datos, Catalog: ¿de dónde sale el producto?

Presentación conoce el formato de entrada. Aplicación coordina. Dominio expresa las reglas de la tienda. Acceso a datos obtiene la información. El reparto permite cambiar la respuesta de la web sin reescribir la multiplicación y cambiar la consulta al catálogo sin revisar el controlador.

Es una aplicación del principio de responsabilidad única: un cambio en el formato de la pantalla y un cambio en la cantidad máxima tienen motivos distintos. Separarlos no obliga a crear una clase por cada línea; permite encontrar un lugar reconocible para cada decisión.

Esta división es una posibilidad habitual, no un número obligatorio de cajas. Una pantalla que solo cambia el nombre de una etiqueta puede necesitar menos estructura. Una compra con promociones y devoluciones probablemente necesita bastante más explicación del dominio.

Primero, la regla que queremos proteger

Usaremos Java 17 o posterior, sin framework. Todos los tipos del artículo se pueden reunir en un archivo LayersExample.java. Los importes son céntimos de euro, sin impuestos, descuentos ni envío; trabajamos con enteros, por lo que aquí no hay redondeo.

record Product(long unitPriceInCents, int maxQuantity) {
    Product {
        if (unitPriceInCents < 0 || maxQuantity < 1) {
            throw new IllegalArgumentException("Invalid product.");
        }
    }

    long quote(int quantity) {
        if (quantity < 1 || quantity > maxQuantity) {
            throw new IllegalArgumentException("Invalid quantity.");
        }
        return Math.multiplyExact(unitPriceInCents, quantity);
    }
}

Product decide qué cantidades admite. Para un producto de 1.999 céntimos con máximo cinco, tres unidades cuestan 5.997. Cero y seis se rechazan. Math.multiplyExact() evita que un importe excesivo se convierta silenciosamente en otro número por desbordamiento.

La regla sigue funcionando si la invocamos desde la web, el panel o una prueba. Esa es la separación que nos interesa: no tener que pasar por una pantalla para recordar el límite. El presupuesto tampoco reserva stock ni garantiza que el precio siga vigente al comprar.

Después, la búsqueda y la coordinación

El catálogo devuelve un producto. Lo dejamos en memoria para que SQL, conexiones y mapeos no tapen la idea:

final class Catalog {
    Product find(String sku) {
        if (!"BOOK-01".equals(sku)) {
            throw new IllegalArgumentException("Unknown product.");
        }
        return new Product(1_999, 5);
    }
}

Un producto inexistente produce un error. Devolver un precio cero lo confundiría con uno gratuito. Hemos escogido una excepción genérica para reducir el ejemplo; una API distinguiría ese resultado de una cantidad inválida para construir su respuesta.

El servicio de aplicación coordina dos pasos: buscar y calcular.

record QuoteService(Catalog catalog) {
    long quote(String sku, int quantity) {
        Product product = catalog.find(sku);
        return product.quote(quantity);
    }
}

Fíjate en lo que se puede leer: «busca el producto y pide su presupuesto». No aparece un código HTTP ni una consulta SQL. La regla de cantidad sigue en Product; copiarla también en el servicio crearía dos versiones que mantener.

Aquí QuoteService depende de una clase concreta, Catalog. Las capas han separado responsabilidades, pero no hemos creado todavía un contrato que permita intercambiar almacenes sin cambiar esa dependencia. Es una decisión visible del ejemplo, no una garantía que aporte cualquier carpeta llamada data.

El artículo de dependencias y contratos ayuda a revisar qué implica esa relación. Aquí basta con conocer una búsqueda pequeña. Si el servicio tuviera que manejar conexiones o interpretar filas, también estaría asumiendo parte del acceso a datos.

La entrada y el resultado

Representaremos la presentación mediante un método que devuelve texto:

record QuoteController(QuoteService service) {
    String quote(String sku, int quantity) {
        return "totalInCents=" + service.quote(sku, quantity);
    }
}

final class LayersExample {
    public static void main(String[] args) {
        var controller = new QuoteController(new QuoteService(new Catalog()));
        System.out.println(controller.quote("BOOK-01", 3));
    }
}

El resultado es totalInCents=5997. Para ejecutarlo, guarda los cuatro bloques juntos y utiliza javac --release 17 LayersExample.java y java LayersExample. El controlador no implementa un servidor: el texto sustituye deliberadamente a la respuesta HTTP.

En una web, presentación también convertiría la cantidad recibida a un entero y traduciría los resultados a HTTP. Que llegue "three" es un problema de formato; que lleguen seis unidades es una restricción del producto. Podemos rechazar pronto una entrada incorrecta y, al mismo tiempo, conservar la regla en el dominio para todos sus consumidores.

Llamar y depender son preguntas distintas

Relaciones de código del presupuesto: QuoteController utiliza QuoteService; este utiliza Catalog y Product; Catalog construye Product.

Las flechas significan utiliza un tipo en el código. El recorrido de ejecución es: controlador, servicio, búsqueda en catálogo, cálculo en producto y respuesta. El producto no necesita conocer quién lo llamó. Tampoco llama al almacenamiento para multiplicar dos números.

En capas cerradas se permiten dependencias solo con la capa inmediatamente inferior; en capas abiertas se admiten saltos definidos. Nuestro reparto permite a aplicación usar dominio y acceso a datos, y al catálogo construir productos. No dibujamos una escalera estricta porque el código no la cumple. Lo importante es acordar unas relaciones concretas y poder revisarlas.

Una capa es una separación lógica. Un tier es una separación de despliegue, como explica la guía N-tier de Microsoft. Estos cuatro tipos pueden ejecutarse en el mismo proceso. Llevar cada uno a un servidor añade red y fallos de comunicación; no mejora por sí mismo el reparto de responsabilidades.

Cuándo deja de ayudar la separación

El controlador del ejemplo es pequeño porque representa una frontera de entrada. Eso no justifica añadir QuoteManager, QuoteProcessor y QuoteFacade si los tres solo reenvían la misma llamada. El presupuesto acabaría necesitando más presentaciones que una reunión de bienvenida.

El criterio de KISS y YAGNI sigue siendo útil al dibujar capas: cada separación debería resolver una dificultad que podamos explicar. Crear nombres nuevos para la misma operación añade recorrido sin repartir mejor sus responsabilidades.

La separación entre presentación, dominio y datos de Fowler ayuda a concentrarse en una responsabilidad. Su valor se aprecia al cambiar algo: un formato de salida lleva al controlador; una regla de cantidad, al producto; una búsqueda, al catálogo. Un cambio de negocio puede afectar a varias piezas, pero deberíamos poder explicar por qué.

Las capas tampoco deciden el primer nivel de carpetas. Podemos agrupar por presentación, aplicación y datos, o reunir las piezas del presupuesto dentro de una funcionalidad, como hacemos con los cortes verticales. Son dos preguntas relacionadas: qué responsabilidad tiene una pieza y junto a qué funcionalidad conviene encontrarla.

Prueba a introducir una segunda entrada que invoque QuoteService. Si utiliza la misma regla sin copiarla ni conocer el formato de la primera pantalla, las capas ya están haciendo un trabajo útil.