🎯 Meta: aprender a organizar aplicaciones grandes para que sigan siendo mantenibles cuando tienen 200 archivos y 5 desarrolladores. La arquitectura es cómo divides y conectas las piezas. Una buena decisión arquitectónica te ahorra meses; una mala te condena a reescribir.
11.1 · ¿Qué es "arquitectura" y por qué importa?
Arquitectura = las decisiones difíciles de cambiar más adelante. Dónde va la lógica, cómo se comunican los módulos, qué depende de qué.
🧠 Analogía: el clean code (cap. 10) es la calidad de cada ladrillo. La arquitectura es el plano del edificio. Puedes tener ladrillos perfectos y un edificio que se cae si el plano es malo; y viceversa. Necesitas ambos.
La regla que gobierna toda buena arquitectura — la Regla de la Dependencia:
Las dependencias apuntan hacia adentro, hacia las reglas de negocio. Lo importante (tu lógica) no debe depender de lo cambiante (framework, BD, HTTP). Es al revés: los detalles dependen de la lógica.
Traducido: tu regla de negocio "un pedido con saldo insuficiente se rechaza" no debe cambiar porque pases de PostgreSQL a MySQL, o de REST a GraphQL. Esos son detalles.
11.2 · Arquitectura en capas (Layered) — la base
La más común y la más fácil de entender. Divides la app en capas horizontales:
┌─────────────────────────────────────────┐
│ PRESENTACIÓN (Controllers / Handlers) │ ← HTTP, JSON, validación de entrada
├─────────────────────────────────────────┤
│ APLICACIÓN / SERVICIOS (casos de uso) │ ← orquesta la lógica de negocio
├─────────────────────────────────────────┤
│ DOMINIO (entidades, reglas) │ ← el corazón: reglas de negocio puras
├─────────────────────────────────────────┤
│ INFRAESTRUCTURA (Repositorios, BD, APIs) │ ← detalles técnicos: PostgreSQL, email...
└─────────────────────────────────────────┘
cada capa solo conoce a la de abajoYa la usaste en NestJS (controller → service → repository), FastAPI y Go. Solo que ahora le pones nombre. Reglas:
- La presentación no habla directo con la BD; pasa por el servicio.
- El dominio no sabe que existe HTTP ni PostgreSQL.
- Cada capa expone una interfaz clara a la de arriba.
💡 Para el 80% de los proyectos, la arquitectura en capas bien hecha es MÁS que suficiente. No saltes a hexagonal/DDD "porque mola". Empieza en capas y evoluciona si el dolor lo pide.
11.3 · Arquitectura Hexagonal (Ports & Adapters)
Lleva la Regla de la Dependencia al extremo: el núcleo (dominio + casos de uso) no depende de nada externo. Se comunica con el mundo a través de puertos (interfaces) que se conectan con adaptadores (implementaciones).
ADAPTADORES ADAPTADORES
(entrada) (salida)
┌──────────┐ ┌──────────┐
│ REST │──┐ ┌──►│PostgreSQL│
└──────────┘ │ ┌──────┐ │ └──────────┘
┌──────────┐ ├─►│ │ │ ┌──────────┐
│ CLI │──┤ │NÚCLEO│──┼──►│ Email │
└──────────┘ │ │dominio│ │ └──────────┘
┌──────────┐ │ │+ casos│ │ ┌──────────┐
│ Cola/MQ │──┘ └──────┘ └──►│ Cache │
└──────────┘ ▲ ▲
puertos (interfaces)- Puerto de entrada: interfaz que el mundo usa para pedirle algo al núcleo (ej.
CrearPedidoUseCase). - Puerto de salida: interfaz que el núcleo usa para pedir cosas al mundo (ej.
PedidoRepositorio,NotificadorEmail). - Adaptadores: las implementaciones concretas (REST, PostgreSQL, SMTP…).
La gran ventaja: puedes cambiar REST por gRPC, o PostgreSQL por MongoDB, sin tocar el núcleo. Y testeas el núcleo con adaptadores falsos (¡otra vez los mocks!).
# Puerto de salida (el núcleo define QUÉ necesita, no CÓMO)
class PedidoRepositorio(ABC):
@abstractmethod
def guardar(self, pedido: Pedido) -> None: ...
# Caso de uso (núcleo puro, sin saber de BD ni HTTP)
class CrearPedido:
def __init__(self, repo: PedidoRepositorio, notificador: Notificador):
self._repo = repo
self._notificador = notificador
def ejecutar(self, datos: DatosPedido) -> Pedido:
pedido = Pedido.crear(datos) # reglas de dominio
if not pedido.es_valido():
raise PedidoInvalido()
self._repo.guardar(pedido) # a través del puerto
self._notificador.avisar(pedido)
return pedido
# Adaptador (infraestructura, intercambiable)
class PedidoRepositorioPostgres(PedidoRepositorio):
def guardar(self, pedido): ... # SQL real aquí11.4 · Clean Architecture
Es una síntesis (de Robert C. Martin) de hexagonal + capas, dibujada como círculos concéntricos. La misma idea: el centro es puro, los bordes son detalles.
┌───────────────────────────────────────┐
│ Frameworks & Drivers (web, BD, UI) │ ← lo más externo y cambiante
│ ┌─────────────────────────────────┐ │
│ │ Adaptadores (controllers, │ │
│ │ presenters, gateways) │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ Casos de uso (aplicación) │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ Entidades (dominio) │ │ │ │ ← el centro, reglas puras
│ │ │ └─────────────────────┘ │ │ │
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└───────────────────────────────────────┘
Las dependencias SIEMPRE apuntan hacia el centro →| Capa | Contiene | Ejemplo |
|---|---|---|
| Entidades | Reglas de negocio de la empresa | Pedido, Usuario con su lógica |
| Casos de uso | Reglas de la aplicación | CrearPedido, CancelarPedido |
| Adaptadores | Traducen entre casos de uso y el mundo | Controllers, Repositorios |
| Frameworks | Herramientas | Laravel, FastAPI, PostgreSQL, Nginx |
🧠 La prueba del algodón: ¿podrías ejecutar tus reglas de negocio en un test sin levantar el framework ni la BD? Si sí, tu arquitectura está limpia. Si para probar "se rechaza pedido sin saldo" necesitas arrancar toda la app y conectar a Postgres, tienes las capas mezcladas.
11.5 · DDD — Domain-Driven Design (en breve)
DDD es una forma de diseñar centrada en el dominio del negocio y su lenguaje. Conceptos clave:
| Concepto | Qué es |
|---|---|
| Lenguaje ubicuo | Código y negocio usan las MISMAS palabras (si el negocio dice "reserva", el código dice Reserva, no BookingEntity) |
| Entidad | Objeto con identidad propia que persiste (Usuario con su id) |
| Value Object | Objeto sin identidad, definido por sus valores (Dinero, Email, Direccion) — inmutable |
| Agregado | Grupo de objetos tratados como una unidad, con una "raíz" que garantiza sus reglas (Pedido + sus Lineas) |
| Repositorio | Abstracción para guardar/recuperar agregados |
| Bounded Context | Una frontera donde un término tiene un significado concreto (en "Ventas" un Cliente es distinto que en "Soporte") |
Value Object — un patrón que usarás mucho:
// En vez de pasar un string suelto (que puede ser cualquier cosa),
// un Value Object garantiza que SIEMPRE es válido:
class Email {
private constructor(private readonly valor: string) {}
static crear(valor: string): Email {
if (!valor.includes('@')) throw new Error('Email inválido');
return new Email(valor.toLowerCase());
}
toString() { return this.valor; }
}
// Ahora, si una función recibe un Email, tienes la GARANTÍA de que es válido.
// La validación vive en un solo sitio.⚠️ DDD es potente pero PESADO. Es para dominios complejos (banca, seguros, logística), con equipos que hablan mucho con expertos del negocio. Para un CRUD o un blog, DDD completo es matar moscas a cañonazos. Toma sus ideas útiles (lenguaje ubicuo, value objects) sin la ceremonia.
11.6 · Monolito vs Microservicios
La gran decisión de nivel sistema:
MONOLITO MICROSERVICIOS
┌────────────────────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ Una sola app │ │Users│ │Order│ │Pay │
│ ┌────┐┌────┐┌────┐ │ └──┬──┘ └──┬──┘ └──┬──┘
│ │User││Ord.││Pay │ │ │ │ │
│ └────┘└────┘└────┘ │ ┌──┴───────┴───────┴──┐
│ Una BD, un deploy │ │ red / API gateway │
└────────────────────┘ └──────────────────────┘
Cada uno su BD y su deploy| Monolito | Microservicios | |
|---|---|---|
| Complejidad inicial | ✅ Baja | ❌ Alta (red, orquestación) |
| Despliegue | ✅ Simple (una cosa) | ❌ Complejo (muchas piezas) |
| Escalado | ⚠️ Todo junto | ✅ Cada servicio por separado |
| Equipos | ⚠️ Se pisan | ✅ Independientes |
| Debugging | ✅ Fácil (todo en un sitio) | ❌ Difícil (rastrear entre servicios) |
🧠 Consejo que vale oro (de los que aprenden por las malas): empieza con un monolito. Casi nadie necesita microservicios al principio, y la complejidad que añaden hunde muchos proyectos jóvenes. Haz un "monolito modular" bien organizado (módulos con fronteras claras); si algún día un módulo necesita escalar o un equipo lo pide, lo extraes. Migrar de monolito ordenado a microservicios es viable; nacer en microservicios sin necesidad es sufrimiento garantizado.
🔗 Tu proyecto CLAINEV ERP usa un enfoque core + plugins: un monolito modular. Es exactamente la estrategia sensata.
11.7 · Patrones de diseño más útiles en backend
No memorices los 23 patrones clásicos. Estos son los que de verdad usarás:
| Patrón | Para qué | Ya lo viste en |
|---|---|---|
| Repository | Abstraer el acceso a datos | Todos los caps de frameworks |
| Dependency Injection | Desacoplar y testear | NestJS, FastAPI, Go |
| Factory | Crear objetos complejos | create_app() de Flask |
| Strategy | Intercambiar algoritmos | El ejemplo de métodos de pago (cap. 10) |
| Adapter | Adaptar una interfaz a otra | Arquitectura hexagonal |
| Observer / Eventos | Reaccionar a sucesos | Eventos de Laravel, triggers de BD |
| Middleware / Pipeline | Encadenar procesamiento | Middleware de todos los frameworks |
💡 Tip: los patrones son vocabulario, no objetivos. No digas "voy a usar Strategy"; di "necesito intercambiar el algoritmo de cálculo" y descubre que eso es Strategy. El patrón es la solución que emerge del problema, no al revés.
11.8 · CQRS y arquitectura orientada a eventos (a alto nivel)
Dos ideas que aparecen mucho al escalar, y que conviene reconocer aunque no las necesites hoy:
CQRS (Command Query Responsibility Segregation): separa el camino de escritura (comandos: "crear pedido", que validan reglas de negocio) del camino de lectura (queries: "listar pedidos", optimizado para leer rápido, a veces desde una tabla/vista desnormalizada distinta).
Escritura (comando) Lectura (query)
POST /pedidos → valida reglas GET /pedidos → lee de una vista
→ escribe en el modelo de dominio optimizada para consultar
→ publica evento "PedidoCreado" (puede ser otra tabla, otro motor)💡 No apliques CQRS "completo" (con dos bases de datos distintas) sin necesidad. La versión ligera —un servicio de comandos y otro de queries dentro del mismo módulo, compartiendo BD— ya resuelve el 90% de los casos: separar "lo que valida y cambia estado" de "lo que solo lee".
Arquitectura orientada a eventos: en vez de que un módulo llame directamente a otro, publica un evento ("PedidoCreado", "PagoConfirmado") y quien esté interesado reacciona, sin acoplarse.
Módulo Pedidos Bus de eventos Módulo Inventario Módulo Notificaciones
"PedidoCreado" ────────► (Kafka/RabbitMQ, ├───────► descuenta stock
apéndice S) └───────► envía email de confirmación🔗 Esto conecta directamente con el apéndice S (Kafka/RabbitMQ) para la mensajería, y con el outbox pattern (mismo apéndice) para publicar eventos de forma consistente con tu transacción de base de datos. Aquí solo necesitas el concepto: los módulos se comunican por eventos, no por llamadas directas, lo que reduce el acoplamiento cuando el sistema crece.
⚠️ El coste real: consistencia eventual (el inventario se actualiza un instante después del pedido, no en la misma transacción) y más piezas que depurar (¿llegó el evento? ¿se procesó?). Como con microservicios (11.6): adopta esto cuando el acoplamiento actual duela de verdad, no por anticipado.
11.9 · Cómo estructurar un proyecto real (plantilla)
Combinando todo, una estructura pragmática por features (no por capas técnicas):
src/
├── modules/
│ ├── pedidos/
│ │ ├── domain/ ← entidades, value objects, interfaces (puertos)
│ │ ├── application/ ← casos de uso (servicios)
│ │ ├── infrastructure/ ← repositorio real, adaptadores
│ │ └── presentation/ ← controllers, DTOs, rutas
│ └── usuarios/
│ └── (misma estructura)
├── shared/ ← código común (errores, utils, config)
└── main.* ← ensamblado / arranque🧠 Organiza por dominio, no por tipo técnico. Prefiere
pedidos/{controller,service,repo}antes quecontrollers/{pedido,usuario}. Así todo lo de una feature vive junto: la cambias sin saltar por 5 carpetas. Es lo que hacen los proyectos que escalan bien.
✅ Ejercicio del capítulo
Rediseña una de tus APIs del blog con arquitectura limpia:
1. Separa en capas: domain / application / infrastructure / presentation.
2. Define el dominio: entidad Articulo con sus reglas (ej. no publicar sin título).
3. Crea al menos un Value Object (ej. Slug o Email).
4. Puerto de salida: interfaz ArticuloRepositorio en domain; implementación
con tu BD en infrastructure.
5. Caso de uso PublicarArticulo que NO conozca HTTP ni la BD directamente.
6. Test del caso de uso con un repositorio falso, SIN levantar el framework.
7. Reflexiona: ¿tu lógica de negocio sobreviviría si cambiaras de framework? Ese
es el objetivo.Con esto sabes construir software que dura. Ahora toca entregarlo al mundo: DevOps.
💡 Pistas de la solución
- El Value Object del paso 3 debe ser inmutable y validarse SOLO en su constructor/factory estático — si en algún punto de tu código validas un
Slugfuera de la propia clase, esa garantía no es real todavía. - Para el test del paso 6 sin levantar el framework: instancia el caso de uso directamente (
new PublicarArticulo(repoFalso, ...)) y llama a.ejecutar(...)— si necesitas un import de NestJS/FastAPI para que el test compile, el dominio todavía depende del framework. - La reflexión del paso 7 es el ejercicio real: si tu caso de uso importa algo de
express/fastapi/@nestjs/common, esa dependencia apunta hacia afuera y rompe la Regla de la Dependencia — aunque el código "funcione".
🧠 Autoevaluación
Que el dominio (tus reglas de negocio) no debe importar ni conocer nada de HTTP, PostgreSQL o un framework concreto. Son los adaptadores (controllers, repositorios) los que dependen del dominio a través de interfaces — nunca al revés. Es lo que te permite testear las reglas de negocio sin levantar la app entera.
Porque los microservicios añaden complejidad real desde el primer día (red, orquestación, consistencia entre servicios, despliegues coordinados) que la mayoría de proyectos no necesita todavía. Un monolito modular bien organizado se puede extraer a microservicios cuando el dolor sea real; nacer en microservicios sin necesidad casi siempre hunde proyectos jóvenes.
Poder ejecutar un test de tus reglas de negocio sin levantar el framework ni conectar a la base de datos real. Si para probar "un pedido sin saldo se rechaza" necesitas arrancar toda la app y una conexión a Postgres, las capas están mezcladas — el dominio depende de detalles que no debería conocer.
Vale la pena en dominios complejos con reglas de negocio intrincadas (banca, seguros, logística) donde el equipo dialoga constantemente con expertos del negocio. Para un CRUD o un blog es sobre-ingeniería: mejor tomar prestadas ideas puntuales (value objects, lenguaje ubicuo) sin la ceremonia completa de agregados y bounded contextos.
Ganas desacoplamiento: Pedidos no necesita saber que Inventario (o Notificaciones) existen, y puedes añadir nuevos suscriptores del evento sin tocar el módulo que lo publica. Pagas consistencia eventual (el stock se descuenta un instante después, no en la misma transacción) y más piezas que depurar — ¿llegó el evento?, ¿se procesó?, ¿falló y hay que reintentar? Por eso se adopta cuando el acoplamiento actual duele de verdad, no por anticipado (mismo criterio que monolito vs microservicios).
Siguiente: 12-docker.md — empaqueta tu app para que corra en cualquier sitio.