Arquitectura en capas para crear aplicaciones legibles, robustas y extensibles

: su código funciona pero la confianza es baja, por lo que duda en tocarlo. Agregar una función significa realizar una cirugía a corazón abierto en la aplicación, modificando la lógica empresarial existente en lugar de ampliar el sistema. Con el tiempo, el costo del cambio sigue aumentando.

¿Esto te resulta familiar?

Los cambios parecen arriesgados porque temes que modificar el código pueda provocar efectos secundarios no deseados. Pasas mucho tiempo desplazándote por archivos grandes, buscando o comprendiendo código. Tienes funciones que “hacen todo” y tienen más de 10 parámetros. Las pruebas se omiten o requieren activar una base de datos, preparar registros manualmente y limpiarlos posteriormente. Rutas FastAPI que construyen consultas SQL.

Es posible que la aplicación aún ofrezca valor, pero se siente frágil. La estructura no está clara, las responsabilidades son confusas y los pequeños cambios parecen desproporcionadamente costosos.

Si esto te resuena, esta publicación es para ti.

TL;DR

Si agregar funciones parece arriesgado o lento, el problema suele ser la estructura, no la calidad del código. La arquitectura en capas separa las responsabilidades y mantiene la lógica empresarial independiente de los marcos y la infraestructura. La división vertical por dominio evita que las capas se conviertan en vertederos a medida que crecen los sistemas. Las capas de aplicación organizan los flujos de trabajo; Las capas de dominio definen el significado y las restricciones. Los límites claros reducen la carga cognitiva, mejoran la capacidad de prueba y abaratan los cambios con el tiempo.

Una buena estructura no añade ceremonia. Conserva el impulso.

El objetivo de este artículo

Debatir si un fragmento de código pertenece a una capa u otra no tiene sentido. El objetivo no es una categorización perfecta sino…

diseñar una aplicación con principios de capas de responsabilidad poco acopladas que hagan que el sistema sea más fácil de entender, probar y evolucionar.

Nuestro objetivo es aplicaciones que sean:

Legible: fácil de navegar y razonar, con baja carga cognitiva. Robusto: las fallas están contenidas, son predecibles y comprensibles. Extensible: se agrega nueva funcionalidad por extensión, no reescribiendo la lógica existente. Los componentes existentes están acoplados libremente, son modulares y reemplazables.

Para llegar allí, estructuraremos la aplicación en capas, cada una con una responsabilidad clara. Esta separación y la forma en que las capas se relacionan entre sí permite que el sistema evolucione con el tiempo.

Descargo de responsabilidad:

Esta no es la mejor ni la única forma de estructurar una aplicación y no es una solución única para todos.

Lo que sigue es una estrategia a la que llegué perfeccionándola a lo largo de varios años en diferentes proyectos. Me inspiré en DDD, los principios SOLID, la arquitectura tipo cebolla, hexagonal y en capas, pero combiné todo en algo que funciona para los tipos de sistemas que normalmente construyo.

Las capas

En la imagen a continuación encontrará una descripción general de la arquitectura en capas:

Relaciones de capas (imagen del autor)

Antes de profundizar en las responsabilidades de cada capa, es útil comprender primero cómo se relacionan entre sí.

Relaciones de capa (dependencias que fluyen hacia adentro)

La capa exterior se puede dividir en dos lados:

Lado de entrada
La capa de interfaz, que recibe datos y actúa como punto de entrada de la aplicación. Lado de salida:
Las capas de repositorio e infraestructura, que pueden comunicarse con sistemas externos como API, bases de datos y colas de mensajes.

La capa de interfaz llama a la capa de aplicación, que es el núcleo del sistema, donde reside la lógica empresarial. La capa de aplicación, a su vez, llama al repositorio y a las capas de infraestructura para conservar los datos o comunicarse externamente.

La conclusión más importante es que las dependencias fluyen hacia adentro.

La lógica empresarial no depende de marcos, bases de datos o mecanismos de transporte. Al aislar la lógica empresarial, ganamos claridad y facilitamos significativamente las pruebas.

capa de dominio

La capa de dominio se centra principalmente en las limitaciones, no en la orquestación ni en los efectos secundarios. Contiene definiciones que deben reflejar el significado comercial y deben ser comprensibles para los empresarios. Piense en clases de datos o modelos Pydantic.

Estos modelos definen la forma y las restricciones de los datos que fluyen a través del sistema. Deben ser estrictos y fracasar tempranamente cuando se violen los supuestos. La validación exhaustiva garantiza datos de alta calidad en el núcleo de nuestro sistema.

Un efecto secundario útil es que los modelos de dominio se convierten en un lenguaje compartido. Es posible que las partes interesadas no técnicas no lean el código línea por línea, pero a menudo pueden comprender la estructura y la intención.

Capa de aplicación

Este es el corazón del sistema.

La capa de aplicación es responsable de orquestar la lógica empresarial y los flujos de trabajo. Se puede llamar desde la capa de interfaz y coordina modelos de dominio, repositorios y servicios de infraestructura para lograr un resultado comercial específico. Además, es responsable de gestionar los fallos a nivel de aplicación y, al mismo tiempo, mantener aislados los problemas de dominio y de infraestructura.

Una buena regla general: si puede realizar una prueba unitaria de esta capa sin activar una base de datos o un servidor web, está en el camino correcto.

Capa de infraestructura

Esta capa contiene todo lo que admite la aplicación pero no contiene lógica empresarial. Piense en esta capa como “herramientas para la capa de aplicaciones”; sólo necesita saber cómo llamar, no cómo se implementa. Por ejemplo, debería poder llamar a send_email(…) sin saber nada sobre la configuración SMTP.

Al desacoplar estas preocupaciones, localiza la complejidad y hace que las integraciones sean más fáciles de reemplazar, actualizar o depurar.

Ejemplos:

configuración de registro utilidades de hash y criptografía clientes http clientes de cola de mensajes remitentes de correo electrónico

Capa de interfaz

La capa de interfaz es la forma en que el mundo exterior habla con su sistema y debe actuar como puerta de entrada para los datos correctos. Piense en una API, CLI, consumidor de cola o algo a lo que pueda llamar un trabajo CRON.

Mantengo estas capas delgadas y sin lógica empresarial. Mi objetivo es solo unas pocas responsabilidades:

Recibir entrada Validar y normalizar (nivel de transporte) entrada (tipos, formato, por ejemplo) Llamar a la capa de aplicación Formatear la respuesta

Capa de repositorio

La capa de repositorio define límites de persistencia (por ejemplo, comunicación con una base de datos). El objetivo es desacoplar la lógica de su aplicación/negocio de una implementación de base de datos particular. Esto incluye modelos ORM, esquemas de bases de datos, consultas SQL y transformaciones relacionadas con la persistencia.

La capa de aplicación no debe ser responsable de:

Qué base de datos utiliza Cómo se escriben las consultas Si los datos provienen de SQL, un caché u otro servicio

La capa de aplicación debería poder simplemente llamar, por ejemplo, get_customer_id(customer_id) y recibir un objeto de dominio a cambio. Esta separación realmente vale la pena cuando necesita cambiar de base de datos, mover la persistencia detrás de una API, agregar almacenamiento en caché o desea realizar pruebas sin una base de datos real.

un edificio muy grande que ha sido destruido
¿Está su aplicación lista para el cambio? (Foto de Jade Koroliuk / Unsplash)

¿Cómo empezar a hacer capas?

Es bastante fácil empezar. Ni siquiera tienes que refactorizar toda tu aplicación de inmediato. Puede ser tan simple como solo 5 carpetas en tu carpeta src en la raíz de tu proyecto:

– src/ – aplicación/ – core.py – dominio/ – cliente.py – infraestructura/ – clima_api_client.py – interfaz/ – api/ – (archivos que contienen FastAPI o Flask, por ejemplo) – cli/ – web/ – (archivos para una aplicación optimizada, por ejemplo – repositorio/ – esquemas.py – cliente_repo.py

Recuerde: el objetivo no es categorizar pedantemente cada fragmento de código en un archivo y dar por terminado el día; los archivos y carpetas separados deben reflejar el hecho de que su sistema está en capas y desacoplado.

Aplicaciones más grandes: capas horizontales por límite de dominio

El ejemplo anterior muestra una aplicación bastante pequeña que tiene capas horizontales únicamente. Esto funciona bien al principio, pero los proyectos más grandes pueden colapsar rápidamente en "módulos de Dios".

Los ingenieros son inteligentes y tomarán atajos bajo presión de tiempo. Para evitar que sus capas se conviertan en vertederos, debe agregar explícitamente el corte vertical por dominio.

Las capas horizontales mejoran la estructura; la división vertical por dominio mejora la escalabilidad.

Las reglas no tienen que ver con restricción o pureza, sino que actúan como barreras de protección para preservar la intención arquitectónica a lo largo del tiempo y mantener el sistema comprensible a medida que crece.

un montón de rocas sentadas en la cima de una montaña
Una pequeña aplicación con 3 capas (Foto de Oghenevwede Okuma / Unsplash)

Aplicar capas horizontales y verticales.

En la práctica, esto significa dividir su aplicación por dominio primero y luego dividirla en capas dentro de cada dominio.

La aplicación del siguiente ejemplo tiene dos dominios: suscripciones y usuarios, ambos divididos en capas.

src/ aplicación/ <– esta es la composición raíz (cableado) main.py suscripciones/ <– este es un dominio dominio/ suscripción.py cancel_reason.py aplicación/ cancel_subscription.py repositorio/ suscripción_repo.py infraestructura/ suscripción_api_client.py interfaz/ api.py usuarios/ <– otro dominio dominio/ aplicación/ repositorio/ interfaz/

En la estructura anterior, main.py es la raíz de composición que importa y llama funciones desde la capa de aplicación en los dominios de usuarios y suscripciones y las conecta a la infraestructura y las interfaces. Esta dependencia fluye hacia adentro, manteniendo los propios dominios independientes.

Reglas básicas

Las capas y los límites de dominio le dan estructura a nuestra aplicación, pero sin algunas reglas básicas la arquitectura colapsa silenciosamente. Sin reglas, el código base vuelve lentamente a un acoplamiento oculto, dependencias circulares y una lógica de dominio que se filtra a través de las fronteras.

Para preservar la estructura a lo largo del tiempo utilizo tres reglas. Estas reglas son intencionalmente simples. Su valor proviene de una aplicación consistente, no de una aplicación estricta:

Regla 1: Los dominios no importan los elementos internos de cada uno.
Si las suscripciones importan usuarios.dominio.Usuario directamente, ya no podrá cambiar los usuarios sin afectar las suscripciones. Debido a que pierde la propiedad clara, esto hace que probar este dominio de forma aislada sea mucho más difícil.

Mueva conceptos verdaderamente compartidos a un dominio compartido o pase datos explícitamente a través de interfaces o DTO (a menudo como ID en lugar de objetos).

Regla 2: los conceptos compartidos van en un dominio compartido
Esto hace que el acoplamiento sea explícito e intencional para evitar que las cosas "compartidas" se dupliquen de manera inconsistente o algo peor: un dominio se convierte silenciosamente en el "núcleo" del que todo depende.

mantenga el dominio pequeño y estable, debería cambiar lentamente, debería contener abstracciones y tipos compartidos, no flujos de trabajo

Regla 3: las dependencias fluyen hacia adentro dentro de cada segmento
Esto mantiene la lógica empresarial independiente de la entrega y la infraestructura.

Notará cuando esta regla se infringe cuando el dominio o el código de la aplicación se inicia dependiendo de FastAPI o una base de datos, las pruebas se volverán lentas y frágiles y las actualizaciones del marco se propagarán a través del código base.

Mantenga las dependencias fluyendo hacia adentro para garantizar que:

Puede intercambiar interfaces e infraestructura. Puede probar la lógica central de forma aislada. Su lógica empresarial sobrevive a los cambios en los extremos.

Fotografía de ángulo bajo de un edificio de gran altura bajo nubes blancas durante el día.
Los edificios altos requerían una arquitectura bien pensada (Foto de Clay LeConey / Unsplash)

Ejemplo práctico: refactorizar un punto final real

Para ilustrar los beneficios, considere un punto final que cancela una suscripción a una revista y devuelve sugerencias alternativas.

La implementación inicial puso todo en un único punto final FastAPI:

Llamadas directas de SQL sin formato a API externas Lógica empresarial integrada en el controlador HTTP

El código funcionó, pero estaba estrechamente acoplado y era difícil de probar. Cualquier prueba requería un servidor web, una base de datos real y una configuración y limpieza exhaustivas.

Diseño refactorizado

Refactorizamos el punto final separando las responsabilidades entre capas.

Capa de interfaz
Ruta API que valida la entrada, llama a la función de la aplicación y asigna excepciones a las respuestas HTTP. Capa de aplicación
Orquesta el flujo de trabajo de cancelación, coordina repositorios y servicios externos y genera errores a nivel de casos de uso. Capa de repositorio
Centraliza el acceso a la base de datos, funciones simples como get_user_email(user_id). Capa de infraestructura
Contiene clientes API para SubscriptionAPI y SuggestionAPI externos, aislados de la lógica empresarial. capa de dominio
Define conceptos básicos como Usuario y Suscripción utilizando modelos estrictos.

Resultado

El punto final se convirtió en un adaptador delgado en lugar de una función divina. La lógica empresarial ahora se puede probar sin necesidad de activar un servidor API o una base de datos. La infraestructura es reemplazable y el código base es más legible.

El cambio es mucho más barato; Las nuevas funciones se crean agregando código nuevo en lugar de reescribir la lógica existente. Los nuevos ingenieros progresan más rápido debido a la reducción de la carga cognitiva. Esto la convierte en una aplicación mucho más sólida que puede evolucionar de forma segura.

Conclusión

El diseño en capas no se trata de agregar ceremonias o perseguir un ideal de libro de texto. Se trata de garantizar que su sistema siga siendo comprensible y adaptable a medida que crece.

Al separar las responsabilidades y mantener las capas poco acopladas, reducimos la carga cognitiva de navegar por el código base. Esto hace que las fallas sean más fáciles de aislar y permite agregar nuevas funciones por extensión en lugar de reescribir la lógica existente.

Estos beneficios se acumulan con el tiempo. Al principio, esta estructura puede parecer un trabajo doble o una sobrecarga innecesaria. Pero a medida que aumenta la complejidad, la recompensa se vuelve clara: los cambios se vuelven más seguros, las pruebas se vuelven más baratas y los equipos se mueven más rápido y con mayor confianza. El sistema permanece estable mientras que las interfaces, la infraestructura y los requisitos pueden cambiar a su alrededor.

En última instancia, una aplicación bien estratificada abarata el cambio. Y, a la larga, eso es lo que mantiene la utilidad del software.

Espero que este artículo haya sido tan claro como pretendía, pero si este no es el caso, hágame saber qué puedo hacer para aclararlo más. Mientras tanto, consulte mis otros artículos sobre todo tipo de temas relacionados con la programación.

¡Feliz codificación!

—Mike

Pd: ¿te gusta lo que estoy haciendo? ¡Sígueme!