Read in English · Lire en français · Auf Deutsch lesen
Sección 5.2 de 5.8 · Intermedio · 24 min de lectura
Arquitectura en capas
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
La arquitectura en capas es el estilo más usado para aplicaciones de negocio, y a menudo se elige por defecto más que por decisión. En esta lección aprenderás cómo funciona, cuánto cuesta y cómo evitar que sus reglas se erosionen.
Piensa en un restaurante. Los clientes solo hablan con los camareros. Los camareros pasan los pedidos a la cocina y nunca entran ellos mismos en el almacén. Los cocineros piden los ingredientes al personal del almacén, y solo el personal del almacén trata con los proveedores. Cada grupo tiene una tarea y solo habla con el grupo de al lado, así que puedes volver a formar a los camareros sin tocar la cocina. Una aplicación en capas funciona igual.
Aquí tienes una aplicación de pedidos de comida en capas. Una solicitud desde el navegador o la app llega a la capa de presentación, que se ocupa de las pantallas, HTTP y JSON. La capa de negocio contiene las reglas y los casos de uso: cómo se calcula el precio de un pedido y cuándo puede cancelarse. La capa de persistencia convierte los objetos de negocio en datos almacenados y viceversa. Solo ella habla con la base de datos. Estas capas son cerradas: una solicitud debe pasar por ellas. La presentación no puede saltarse la capa de negocio para llegar a la persistencia, aunque así fuera más rápido de escribir. Y ninguna capa puede llamar nunca hacia arriba. La capa de servicios, para correos electrónicos, recibos en PDF y registros de auditoría, es abierta. Solo algunas operaciones la necesitan, así que la capa de negocio puede llamarla o pasar de largo directamente a la persistencia.
Las capas cerradas te dan capas de aislamiento. Como la presentación solo conoce la interfaz de la capa de negocio, el equipo puede reestructurar las tablas, o incluso cambiar de producto de base de datos, sin tocar ni una sola pantalla. Cada excepción que permite a una capa saltarse otra debilita este aislamiento. Si los controladores consultan directamente la base de datos, un cambio de esquema se propaga ahora también a la capa de presentación.
Sigamos una solicitud. Un cliente hace clic en Hacer pedido. La capa de presentación analiza la solicitud, comprueba que el usuario ha iniciado sesión y llama al servicio de pedidos. No sabe nada de tablas ni de precios. La capa de negocio comprueba que el restaurante está abierto, calcula el precio del carrito y pide al repositorio que guarde el pedido. También usa la capa abierta de servicios para enviar un recibo por correo electrónico. La capa de persistencia convierte el pedido en filas y las escribe en una sola transacción de base de datos. PostgreSQL confirma, y el resultado sube de vuelta. El cliente ve su número de pedido. Ahora el equipo de producto pide una propina para el repartidor. Eso necesita un campo en la solicitud, una regla de propina máxima, una columna nueva y una migración. Cuatro capas para una funcionalidad pequeña: el costo del particionamiento técnico.
La organización en capas tiene un antipatrón clásico, el sumidero de arquitectura: solicitudes que pasan por capas que solo las reenvían. Ver una carta es el caso típico. El controlador la pasa, el servicio la pasa, y solo el repositorio hace algún trabajo. Compáralo con hacer un pedido, donde cada capa añade comportamiento: validar, calcular el precio y comprobar reglas, y luego guardar en una transacción. Algo de paso directo es el precio del aislamiento. Como regla general, en torno a una quinta parte de las solicitudes es normal. Si la mayoría de las solicitudes son de simple paso, probablemente la organización en capas es el estilo equivocado.
El estilo puntúa alto en sencillez y bajo costo: lo conocen casi todos los desarrolladores, con una base de código, un despliegue y una base de datos. Puntúa bajo en desplegabilidad, escalabilidad y tolerancia a fallos. Cualquier cambio vuelve a desplegarlo todo, toda la aplicación escala como una sola, y una fuga de memoria puede tumbarlo todo. Encaja bien en aplicaciones pequeñas y medianas construidas por un solo equipo, y en aplicaciones que son sobre todo formularios sobre datos. Encaja mal como nivel superior de un sistema grande con varios equipos, porque todos los equipos trabajan en todas las capas. El siguiente paso habitual es un monolito modular, con capas dentro de cada módulo.
Las reglas de las capas se deterioran rápido. Alguien importa el repositorio directamente en un controlador para ahorrarse veinte minutos, nadie lo nota, y el siguiente desarrollador lo copia. La defensa es una prueba de arquitectura: una comprobación automatizada de las dependencias que se ejecuta en la compilación y la hace fallar cuando se incumple una regla. En Python, import-linter lee las reglas de un archivo de configuración. En Java, ArchUnit las expresa como una prueba unitaria. Empieza registrando las infracciones existentes como excepciones conocidas, para que solo las nuevas hagan fallar la compilación. Después, reduce las excepciones con el tiempo.
La sección completa tiene la tabla de características, más antipatrones y una configuración de import-linter que funciona. A continuación, en la Sección cinco punto tres, mantenemos un único despliegue pero lo dividimos por dominio de negocio: el monolito modular. Nos vemos allí.
Lo que necesitas saber
- Una arquitectura en capas organiza el código en capas horizontales por función técnica, normalmente presentación, negocio, persistencia y base de datos, y permite que cada capa dependa solo de las capas que tiene debajo.
- Las capas cerradas no deben saltarse, lo que proporciona capas de aislamiento: un cambio dentro de una capa no se propaga a las demás mientras su interfaz se mantenga. Una capa abierta, como una capa de servicios compartidos, puede saltarse.
- La organización en capas es particionamiento técnico, así que una funcionalidad de negocio típica toca todas las capas; esto ralentiza el cambio y favorece estructuras de equipo divididas por capa.
- El antipatrón del sumidero de arquitectura consiste en solicitudes que pasan por capas que solo las reenvían; como regla general, que una minoría de solicitudes sea así es normal, y una gran mayoría sugiere que el estilo no es el adecuado.
- El estilo puntúa alto en sencillez y bajo costo, y bajo en desplegabilidad, escalabilidad, elasticidad y tolerancia a fallos, porque todo el sistema es una sola unidad de despliegue.
- Las reglas de las capas se erosionan si no se hacen cumplir automáticamente, por ejemplo con import-linter en Python o ArchUnit en Java, como pruebas que hacen fallar la compilación.
Definición y origen
Una arquitectura en capas organiza una aplicación en capas horizontales, cada una de las cuales tiene una responsabilidad técnica, ofrece servicios a la capa superior y usa los servicios de la capa inferior. Es el estilo más usado para aplicaciones de negocio, a menudo por defecto más que por decisión, porque la mayoría de los frameworks web generan un proyecto con carpetas para las vistas, la lógica de negocio y los modelos.
La idea es antigua y procede del software de sistemas. Edsger Dijkstra describió el sistema operativo multiprogramado THE (Communications of the ACM, 1968) como una jerarquía de niveles, cada uno construido solo sobre los que tiene debajo, de modo que cada nivel pudiera analizarse y probarse por sí solo. Las pilas de protocolos de red, como el modelo de referencia OSI, usan el mismo principio. En el software de negocio, la organización en capas llegó con los sistemas cliente-servidor y de tres niveles en la década de 1990, y POSA (1996) la catalogó como el patrón Capas (Layers). Garlan y Shaw incluyeron los sistemas en capas entre sus estilos básicos, con la restricción de que las interacciones se limitan a capas adyacentes.
La aplicación de negocio típica tiene cuatro capas. La capa de presentación se ocupa de la interfaz de usuario y del protocolo: solicitudes HTTP, pantallas, JSON. La capa de negocio contiene las reglas y los casos de uso: cómo se calcula el precio de un pedido, cuándo puede cancelarse. La capa de persistencia convierte los objetos de negocio en datos almacenados y viceversa, normalmente mediante repositorios y un mapeador objeto-relacional. La capa de base de datos es la propia base de datos. Las aplicaciones pequeñas a veces fusionan negocio y persistencia; las más grandes añaden capas, como una capa de aplicación que coordina los casos de uso por encima de una capa de dominio que contiene el modelo central. El estilo no fija el número; fija la dirección de las dependencias.
Una aplicación de pedidos de comida en capas
Cómo funciona: capas cerradas, capas abiertas y aislamiento
Cada capa puede ser cerrada o abierta. Una solicitud debe pasar por una capa cerrada: la capa de presentación no puede saltarse la capa de negocio para llegar a la capa de persistencia, aunque así fuera más rápido de escribir. Una capa abierta se puede saltar. El ejemplo habitual es una capa de servicios técnicos compartidos, como enviar correos electrónicos, generar recibos en PDF o escribir registros de auditoría. Solo algunas operaciones de negocio los necesitan, así que obligar a cada solicitud a pasar por esa capa añadiría código de reenvío inútil. En el diagrama, la capa de negocio puede llamar a la capa de servicios o pasar de largo directamente a la persistencia.
Las capas cerradas proporcionan capas de aislamiento. Como la capa de presentación solo conoce la interfaz de la capa de negocio, el equipo puede sustituir el mapeador objeto-relacional, reestructurar las tablas o pasar de un producto de base de datos a otro sin tocar ni un solo controlador o plantilla, siempre que la interfaz de la capa de negocio siga igual. Es la ocultación de información (David Parnas, 1972) aplicada a todo un estrato del código, y es el principal beneficio que ofrece el estilo. Cada capa abierta, y cada excepción que permite a una capa saltarse otra, lo debilita: si los controladores consultan directamente la base de datos, un cambio de esquema se propaga ahora también a la capa de presentación.
La dirección de las dependencias es la otra mitad de la regla. Las capas inferiores no deben saber nada de las superiores: la capa de persistencia nunca debe importar un controlador, y la capa de negocio nunca debe construir HTML. Cuando una capa inferior necesita avisar a una superior, lo hace a través de una abstracción que pertenece a la capa inferior, como una interfaz de callback o un evento, de modo que en el código la dependencia siga apuntando hacia abajo. La Sección 3.4 trata en profundidad la dirección de las dependencias, los ciclos y la organización en capas estricta frente a la relajada.
Conviene no confundir capas y niveles. Una capa es una agrupación lógica en el código. Un nivel (tier) es un límite físico en tiempo de ejecución, como un servidor o un proceso separado. Un sistema clásico de tres niveles ejecuta el navegador, un servidor de aplicaciones y un servidor de base de datos como tres niveles, mientras que el servidor de aplicaciones por sí solo puede contener tres o cuatro capas. Convertir una capa en un nivel pone una red entre dos capas que antes se llamaban en memoria, lo que multiplica el costo de cada llamada; rara vez merece la pena hacerlo por sí mismo.
Ejemplo: una aplicación web clásica
Piensa en el sitio web de pedidos de una empresa regional de reparto de comida: los clientes consultan las cartas, hacen pedidos y pagan; el personal de los restaurantes ve los pedidos entrantes. Un equipo de cinco personas lo construye con un framework web convencional y una base de datos PostgreSQL. Una estructura en capas es el punto de partida obvio, y la organización del código hace visibles las capas.
foodshop/
presentation/ # HTTP only: parse requests, render responses
order_views.py
menu_views.py
templates/
business/ # rules and use cases; no HTTP, no SQL
order_service.py
pricing.py
services/ # open layer: shared technical services
email.py
receipts.py
persistence/ # all database access
order_repository.py
menu_repository.py
models.py
settings.pyEjemplo práctico
Ejemplo práctico: seguir una solicitud capa a capa hacia abajo
Un cliente hace clic en Hacer pedido. La solicitud llega como POST /orders con el carrito y la dirección de entrega.
| Presentación | order_views.place_order analiza el JSON, comprueba que está bien formado y que el usuario ha iniciado sesión, y llama a order_service.place(customer_id, items, address). No sabe nada de tablas ni de precios. |
|---|---|
| Negocio | order_service.place comprueba que el restaurante está abierto, calcula el precio del carrito con pricing.py (descuentos, gastos de envío, impuestos) y pide a order_repository que guarde el pedido. Llama a receipts.send en la capa abierta de servicios para enviar un recibo por correo electrónico. |
| Persistencia | order_repository.save convierte el pedido en filas de orders y order_lines y las escribe en una sola transacción de base de datos. |
| Base de datos | PostgreSQL confirma la transacción y responde. El resultado sube de vuelta: el servicio devuelve el nuevo pedido y la vista genera 201 Created con el número de pedido. |
| Solicitud de cambio | Producto pide una propina para el repartidor. El desarrollador añade un campo de propina a la solicitud y a la respuesta (presentación), una regla de propina máxima y la propina en el total (negocio), y una columna y un mapeo (persistencia), además de una migración (base de datos). Cuatro capas para una funcionalidad pequeña. |
Cada capa hizo un solo tipo de trabajo, y cada una pudo probarse por separado sustituyendo la capa inferior por un doble de prueba.
La propina muestra el costo del particionamiento técnico: incluso un pequeño cambio de negocio atraviesa todas las capas, así que toca más archivos, más revisores y, si los equipos están divididos por capa, más equipos.
Las consecuencias del particionamiento técnico
Como la organización en capas es particionamiento técnico, el dominio de negocio queda repartido entre las capas. No hay un único lugar llamado entrega
: hay controladores de entrega, servicios de entrega y repositorios de entrega, intercalados con todo lo demás en cada capa. Un desarrollador que quiera entender cómo funcionan las propinas tiene que leerlos todos. A medida que el sistema crece, cada capa se convierte en una colección grande y de baja cohesión de código sin relación: la capa de negocio contiene juntas las reglas de pedidos, pagos, cartas y repartidores, y nada impide que el código de pagos llame a las profundidades del código de cartas. Con el tiempo, las capas siguen limpias, pero el interior de cada una se convierte en una gran bola de barro.
También hay un costo organizativo. Organizar los equipos por capa (un equipo de frontend, un equipo de backend, un equipo de base de datos) significa que casi cada funcionalidad necesita un traspaso y una cola entre equipos, algo que la ley de Conway acaba consolidando en la arquitectura. Martin Fowler considera por este motivo que los equipos organizados por capa son un antipatrón: los desarrolladores no tienen por qué ser full-stack, pero los equipos sí. Una arquitectura en capas no obliga a dividir los equipos por capa, pero hace que sea el camino de menor resistencia.
Por último, un monolito en capas es un solo cuanto de arquitectura: una unidad de despliegue y normalmente una base de datos. Cada cambio, por pequeño que sea, significa compilar, probar y volver a desplegar toda la aplicación, y toda la aplicación escala y falla como una sola. A menudo eso es perfectamente aceptable, pero pone un techo.
El sumidero de arquitectura y otros antipatrones
El antipatrón del sumidero de arquitectura (architecture sinkhole), un nombre popularizado por Mark Richards, describe solicitudes que fluyen por capas que no hacen nada más que pasarlas a la siguiente. Ver una carta es el caso típico: el controlador llama a un método de servicio que llama a un método de repositorio que ejecuta una consulta, y el controlador y el servicio no aportan nada. Cada capa de simple paso añade código que escribir, probar y leer, y un poco de latencia. Algo de paso directo es el precio del aislamiento y no supone un problema. Richards propone una regla general: si aproximadamente una quinta parte de las solicitudes son de simple paso, es normal; si lo son la mayoría, probablemente la aplicación es sobre todo acceso a datos con poca lógica de negocio, y la organización en capas es el estilo equivocado o habría que abrir las capas. Abrir capas, sin embargo, supone renunciar al aislamiento, así que decídelo de forma deliberada y deja constancia del motivo.
Vale la pena reconocer pronto otros antipatrones habituales. Capas con fugas: SQL en los controladores, objetos de solicitud HTTP pasados a los servicios o entidades del ORM convertidas directamente en JSON, de modo que un cambio en una capa se propaga a otras. Dependencias hacia arriba: un repositorio que llama a un servicio, o un objeto de dominio que da formato a HTML. La capa de negocio anémica: servicios que solo mueven datos de un lado a otro mientras las reglas reales viven en los controladores o en procedimientos almacenados; Fowler bautizó el antipatrón relacionado Anemic Domain Model (modelo de dominio anémico) en 2003. El servicio dios: un único OrderService de varios miles de líneas al que llaman todos los demás servicios. La capa de utilidades de la que todo depende: un paquete common que poco a poco acumula lógica de negocio y acopla todo con todo.
Sumidero frente a organización en capas útil
Características del estilo en capas
| Característica | Valoración | Motivo |
|---|---|---|
| Sencillez | Alto | Conocido por casi todos los desarrolladores y estructura predeterminada de la mayoría de los frameworks; fácil de explicar y de empezar. |
| Costo total | Alta (barato) | Una base de código, un despliegue, una base de datos; infraestructura y operación mínimas. |
| Capacidad de prueba (testabilidad) | Media | Cada capa puede probarse sustituyendo la capa inferior por un doble de prueba, pero las pruebas realistas a menudo necesitan toda la pila y una base de datos. |
| Modificabilidad ante cambios de negocio | De baja a media | Los cambios técnicos se quedan dentro de una capa, pero la mayoría de los cambios de funcionalidad cruzan todas las capas. |
| Desplegabilidad | Bajo | Cualquier cambio obliga a volver a desplegar toda la aplicación; el riesgo de cada lanzamiento crece con el tamaño. |
| Escalabilidad y elasticidad | Bajo | Toda la aplicación escala como una sola unidad; no puedes escalar solo la parte con más carga, y el arranque de una aplicación grande ralentiza el escalado elástico. |
| Tolerancia a fallos | Bajo | Una sola fuga de memoria o una consulta descontrolada pueden tumbar todo el proceso; no hay aislamiento entre funcionalidades. |
| Rendimiento | Media | Las llamadas en proceso son rápidas, pero las capas adicionales y el mapeo de objetos añaden sobrecarga, y los sumideros añaden más. |
| Capacidad de evolución a gran escala | Bajo | La baja cohesión dentro de cada capa y el acoplamiento entre funcionalidades hacen que los sistemas grandes en capas sean difíciles de reestructurar. |
Cuándo encaja bien y cuándo no
La arquitectura en capas es una buena opción para aplicaciones pequeñas y medianas construidas por un solo equipo, para productos cuyo dominio todavía no se entiende bien (el particionamiento por dominio necesita límites que aún no conoces), para presupuestos y plazos ajustados y para aplicaciones que son sobre todo formularios sobre datos, con reglas modestas. También es una estructura interna razonable para un único módulo o servicio que forma parte, a su vez, de un sistema con particionamiento por dominio: un módulo delivery en capas dentro de un monolito modular obtiene lo mejor de ambos.
Es una mala opción como estructura de nivel superior de un sistema grande con varios equipos, porque todos los equipos trabajan en todas las capas y las funcionalidades chocan. Es mala cuando partes del sistema necesitan una escalabilidad o una disponibilidad muy distintas, porque solo hay un cuanto. Y es mala cuando la lógica de negocio es rica y central, porque las reglas quedan dispersas entre controladores, servicios y procedimientos almacenados; en ese caso, una estructura interna centrada en el dominio, como la arquitectura hexagonal o limpia, mantiene el núcleo independiente de la base de datos y del framework web.
La evolución más habitual es pasar de un monolito en capas a un monolito modular: mantener un único despliegue, pero reorganizar el nivel superior por dominio y conservar las capas dentro de cada módulo.
Hacer cumplir las reglas de las capas en el código
Las reglas de las capas se deterioran rápido. Con la presión de una fecha de entrega, un desarrollador importa el repositorio directamente en un controlador porque le ahorra veinte minutos, nadie lo nota en la revisión y el siguiente desarrollador copia el patrón. La defensa es una prueba de arquitectura: una comprobación automatizada de las dependencias que se ejecuta en la compilación y la hace fallar cuando se incumple una regla, lo que convierte la regla en una función de aptitud.
En Python, import-linter lee contratos de un archivo de configuración. Su contrato layers enumera las capas de la más alta a la más baja y prohíbe que una capa inferior importe una superior. Por sí solo no hace que las capas sean cerradas (una capa superior puede importar cualquier capa inferior), así que un contrato forbidden añade la regla de que la presentación no debe importar directamente la persistencia; necesita allow_indirect_imports activado, porque de lo contrario la cadena normal de la presentación a la persistencia pasando por el negocio contaría como una infracción. En Java, ArchUnit expresa las mismas reglas como una prueba unitaria: layeredArchitecture() declara las capas por paquete y establece, por ejemplo, que solo la capa de servicios puede acceder a la capa de persistencia. Existen herramientas similares para otros ecosistemas, como dependency-cruiser para JavaScript y TypeScript.
Dos prácticas hacen que las comprobaciones perduren. Introdúcelas registrando las infracciones existentes como excepciones conocidas, para que la compilación pase a verde de inmediato y solo las infracciones nuevas la hagan fallar; después, reduce las excepciones con el tiempo. Y mantén la configuración junto al código y revisa sus cambios como si fueran código, porque relajar una regla es una decisión arquitectónica que merece un registro de decisiones.
[importlinter]
root_package = foodshop
[importlinter:contract:layers]
name = Layers only depend downwards
type = layers
layers =
foodshop.presentation
foodshop.business
foodshop.services
foodshop.persistence
[importlinter:contract:closed-business]
name = Presentation must go through business
type = forbidden
source_modules = foodshop.presentation
forbidden_modules = foodshop.persistence
# presentation -> business -> persistence is the normal path
allow_indirect_imports = TrueComprueba lo que has entendido
¿Qué diferencia hay entre una capa cerrada y una capa abierta?
Una solicitud debe pasar por una capa cerrada y no puede saltársela; la capa superior puede saltarse una capa abierta, algo típico de una capa de servicios compartidos que solo necesitan algunas solicitudes.
¿Por qué una aplicación en capas tiende a necesitar cambios en todas las capas para una nueva funcionalidad?
Porque tiene particionamiento técnico: el código de cada capacidad de negocio está repartido entre las capas de presentación, negocio y persistencia, y la mayoría de las funcionalidades se definen en términos de negocio.
¿Qué diferencia hay entre una capa y un nivel (tier)?
Una capa es una agrupación lógica del código por responsabilidad; un nivel es un límite físico en tiempo de ejecución, como un servidor separado. Normalmente, varias capas se ejecutan dentro de un mismo nivel.
¿Por qué un contrato layers de import-linter, por sí solo, no hace que las capas sean cerradas?
Solo prohíbe que las capas inferiores importen las superiores; una capa superior puede seguir importando cualquier capa inferior, así que hace falta un contrato forbidden aparte para impedir que la presentación importe la persistencia.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “La mayoría de nuestras solicitudes solo pasan del controlador al servicio y al repositorio sin hacer nada. ¿Deberíamos eliminar la capa de servicios?”