Arquitectura de software¶
De un vistazo
Materia: Capa 2 · Dar forma al sistema · Tiempo de lectura: ~35 min · Requisitos previos: toda la Capa 2 anterior y DDD.
En una frase: vas a entender que la arquitectura no es un diagrama de cajas ni una moda que adoptas, sino el conjunto de decisiones caras de cambiar que tomas para gestionar el cambio futuro y los tradeoffs, y por qué la mayoría de reglas famosas (hexagonal, Clean, microservicios) son solo respuestas concretas a esa misma pregunta.
Por qué esto importa¶
Llevas tiempo escribiendo código que funciona. Clases con buen nombre, funciones cortas, tests. Y aun así, un día te pasa esto: te piden "cambiar la pasarela de pago de Stripe a Redsys" y descubres que Stripe está metido en 40 sitios —en el controlador, en la entidad Pedido, en un email, en un cron—. Lo que parecía un cambio de un día se convierte en tres semanas de arqueología con miedo a romper algo.
Eso no es un problema de código. Cada una de esas 40 clases está bien escrita. Es un problema de arquitectura: nadie decidió dónde vive la dependencia de Stripe, así que se filtró por todas partes.
La arquitectura es lo que responde a las preguntas que el código bien escrito, por sí solo, no responde:
- ¿Por qué puedo cambiar la base de datos de MySQL a PostgreSQL en un proyecto sin tocar la lógica de negocio, y en otro es un infierno?
- ¿Por qué "partir el monolito en microservicios" resolvió los problemas de un equipo y hundió a otro?
- ¿Por qué un sistema con tests, CI y buen estilo se vuelve imposible de tocar a los dos años?
- ¿Por qué mi decisión de "meter la lógica en el modelo de Doctrine" me parecía limpia y ahora me tiene atrapado?
Todas tienen la misma raíz: qué decisiones se tomaron sobre la estructura, y cuánto cuesta ahora deshacerlas. Sin este modelo mental, "arquitectura" es dibujar cajas bonitas y copiar el diagrama de la charla de moda. Con él, es una disciplina de gestión de riesgo: qué me juego, qué me reservo poder cambiar, qué precio pago por cada cosa. Ese es el salto de "sé programar" a "sé construir sistemas que aguanten años".
Intuición primero: el edificio y las tuberías¶
Piensa en un edificio de oficinas.
Hay decisiones que tomas una vez y que te comprometen para siempre: dónde van las columnas maestras, cuántas plantas, dónde están los huecos de los ascensores y las bajantes. Cambiar eso después no es "una reforma", es tirar el edificio. Son caras de cambiar porque todo lo demás se apoya en ellas.
Y hay decisiones que son baratas de cambiar en cualquier momento: el color de las paredes, dónde pones las mesas, qué moqueta. Son detalles. Importan para vivir ahí, pero no condicionan el resto.
La arquitectura de un edificio es el conjunto de decisiones del primer grupo: las estructurales, las que quedan cuando quitas la pintura y los muebles. La arquitectura de un sistema software es exactamente lo mismo: las decisiones que son caras de revertir.
graph LR
subgraph ARQ["ARQUITECTURA · caro de cambiar"]
A["¿Dónde vive el dominio?<br/>¿Qué depende de qué?<br/>¿Un proceso o veinte?<br/>¿Cómo se comunican?"]
end
subgraph DET["DETALLES · barato de cambiar"]
B["Qué framework MVC<br/>Qué ORM<br/>Nombre de esta clase<br/>Este if o este switch"]
end
ARQ -->|"condiciona"| DET
Fíjate en la consecuencia incómoda: si algo es fácil de cambiar, no es decisión arquitectónica, por mucho que salga en el diagrama. Que uses Symfony o Laravel es, en el buen diseño, un detalle. Que tu lógica de negocio dependa de Symfony o no —eso sí es arquitectura, porque determina si algún día puedes cambiarlo sin morir.
Guarda esta imagen: arquitectura = lo estructural = lo caro de cambiar. Todo lo que sigue cuelga de aquí.
El detalle¶
1. Qué es (y qué no es) la arquitectura¶
La definición que vamos a usar, y que aguanta el peso de toda la lección, la debemos sobre todo a Ralph Johnson y la popularizó Martin Fowler:
La arquitectura es el conjunto de decisiones de diseño que son importantes y difíciles de cambiar —las que, si te equivocas, cuestan mucho corregir después.
Tres matices que la gente se salta:
- No es "lo grande" ni "lo de arriba". No se trata de tamaño. Una decisión sobre cómo modelas una regla de negocio central puede ser más arquitectónica que el diagrama de despliegue entero, si es más cara de cambiar.
- Es contextual y cambia con el tiempo. Lo que es caro de cambiar depende de tu sistema, tu equipo y tu momento. En una startup de tres personas partir en microservicios es carísimo (no tienes manos); en una empresa de 300 ingenieros, mantener todo en un monolito acoplado también es carísimo. La misma decisión, tradeoff opuesto.
- Es lo que queda cuando quitas los detalles. Robert C. Martin lo dice bien: si te enseñan el código de un sistema y lo primero que ves es "Symfony, Doctrine, Twig", te han enseñado el framework, no la arquitectura. La arquitectura debería gritar de qué va el sistema —"esto es un sistema de reservas hoteleras"— antes de gritar con qué herramientas está hecho.
El test de la reversibilidad
Ante cualquier decisión, pregúntate: "si dentro de un año esto resulta ser un error, ¿cuánto me cuesta deshacerlo?". Si la respuesta es "una tarde", no pierdas el sueño: decide rápido y sigue. Si es "medio año y rezar", ahí es donde tienes que pensar despacio, buscar opiniones y quizá escribir un ADR (lo vemos al final). El buen arquitecto no piensa mucho en todo; piensa mucho solo en lo caro de revertir, y trata lo demás como los detalles baratos que son.
2. Acoplamiento y cohesión, pero a nivel de sistema¶
Ya conoces acoplamiento (cuánto depende una cosa de otra) y cohesión (cuánto tienen que ver entre sí las cosas que están juntas) del diseño de clases. La arquitectura es la misma idea subida un nivel: ya no hablamos de si dos clases están acopladas, sino de si dos módulos, capas o servicios enteros lo están.
- Alta cohesión a nivel de sistema: todo lo que tiene que ver con "facturación" vive junto y se puede razonar, cambiar y desplegar como una unidad. Si para tocar una regla de facturación tienes que abrir cinco módulos que no se llaman "facturación", tu cohesión es mala.
- Bajo acoplamiento a nivel de sistema: el módulo de facturación puede cambiar por dentro sin obligar a "envíos", "catálogo" y "usuarios" a cambiar con él.
La regla de oro, la misma de siempre pero que a esta escala se paga carísima:
Junta lo que cambia junto, separa lo que cambia por razones distintas. Un buen límite arquitectónico es aquel que casi nunca tienes que cruzar para hacer un cambio.
El acoplamiento malo a nivel de sistema es especialmente traicionero porque no lo ves en una clase: lo ves cuando un cambio "pequeño" te obliga a desplegar cuatro servicios coordinados, o cuando nadie se atreve a tocar un módulo porque "quién sabe qué depende de esto". Ese miedo es el síntoma clínico del acoplamiento arquitectónico.
La pregunta que revela tu arquitectura real
No mires el diagrama que dibujó alguien. Mira el historial de git: ¿qué ficheros cambian siempre juntos? Si Pedido.php, EmailService.php y StripeClient.php aparecen en el mismo commit una y otra vez, están acoplados de verdad, aunque el diagrama los pinte separados. La arquitectura real es la que revelan los cambios, no la del PowerPoint.
3. Separación de responsabilidades y arquitecturas en capas¶
La respuesta más antigua y más común al problema del acoplamiento es separar por responsabilidades y organizar el sistema en capas, donde cada capa tiene un trabajo y solo habla con la de al lado:
graph TB
P["Presentación<br/>(controladores, API, vistas)"] --> N["Aplicación / Negocio<br/>(casos de uso, reglas)"]
N --> D["Acceso a datos<br/>(repositorios, ORM)"]
D --> BD[("Base de datos")]
La idea es sana: la capa de presentación no debería saber SQL, y la base de datos no debería saber qué es un botón. Cada una oculta sus detalles a las de arriba. Si separas bien, puedes cambiar la vista sin tocar la lógica, o la lógica sin tocar la vista.
Pero las capas clásicas tienen un defecto sutil y grave que casi nadie ve hasta que le muerde: la flecha de dependencia va hacia abajo, hacia la base de datos. Eso significa que tu capa de negocio —lo más valioso y estable que tienes— depende de la capa de acceso a datos, que a su vez depende de una tecnología concreta (Doctrine, MySQL). Tu dominio acaba atado a la infraestructura. Cuando quieres cambiar el ORM o meter una cola de mensajes, la onda expansiva sube hasta el negocio.
Dicho de otra forma: en la arquitectura en capas tradicional, la base de datos está en el centro y todo gira a su alrededor. Y la base de datos es un detalle (recuerda: cambiable, aunque duela), no el corazón de tu sistema. Ponerla en el centro es poner el detalle en el trono. Las siguientes dos secciones son la corrección de ese error.
4. Arquitectura hexagonal: puertos y adaptadores¶
La arquitectura hexagonal (Alistair Cockburn, 2005), también llamada puertos y adaptadores, arregla el defecto de las capas con una inversión brutal y simple: pon el dominio en el centro y haz que todo lo demás dependa de él, nunca al revés.
La imagen: tu dominio (las reglas de negocio, lo que aprendiste en DDD: entidades, agregados, servicios de dominio) está en el centro del hexágono, sin saber nada del mundo exterior. No sabe si lo llama una API REST o un comando de consola. No sabe si guarda en MySQL o en un fichero. Se comunica con fuera solo a través de puertos.
- Un puerto es una interfaz que define el dominio: "yo necesito poder guardar pedidos" →
interface RepositorioPedidos. El puerto pertenece al dominio y habla su idioma. - Un adaptador es la implementación concreta de ese puerto, que vive fuera:
RepositorioPedidosDoctrine,RepositorioPedidosEnMemoria. El adaptador conoce la tecnología; el puerto no.
graph LR
subgraph FUERA_IZQ["Adaptadores que empujan (driving)"]
API["Controlador REST"]
CLI["Comando consola"]
TEST["Test"]
end
subgraph NUCLEO["Núcleo · el hexágono"]
PORT_IN["Puerto de entrada<br/>(caso de uso)"] --> DOM["DOMINIO<br/>reglas de negocio<br/>(no sabe nada de fuera)"]
DOM --> PORT_OUT["Puerto de salida<br/>(interfaz Repositorio)"]
end
subgraph FUERA_DER["Adaptadores conducidos (driven)"]
DB["Repositorio Doctrine"]
MAIL["Cliente email SMTP"]
PAY["Cliente Stripe"]
end
API --> PORT_IN
CLI --> PORT_IN
TEST --> PORT_IN
PORT_OUT -.implementado por.-> DB
PORT_OUT -.implementado por.-> MAIL
PORT_OUT -.implementado por.-> PAY
La clave está en el sentido de las flechas de dependencia: el dominio define la interfaz, la infraestructura la implementa. Esto es la inversión de dependencias aplicada a la arquitectura. El dominio dice "necesito algo que sepa guardar pedidos" y no le importa quién. Doctrine se pliega al dominio, no al revés.
En Symfony esto se ve limpísimo con la inyección de dependencias:
// Dominio — no sabe que Doctrine existe. Este es el PUERTO.
namespace App\Dominio\Pedido;
interface RepositorioPedidos
{
public function guardar(Pedido $pedido): void;
public function porId(PedidoId $id): ?Pedido;
}
// Aplicación — el caso de uso depende del PUERTO (la interfaz), no del adaptador.
namespace App\Aplicacion\Pedido;
final class ConfirmarPedido
{
public function __construct(
private RepositorioPedidos $pedidos, // ← interfaz del dominio
private PasarelaPago $pasarela, // ← otro puerto
) {}
public function __invoke(ConfirmarPedidoCommand $cmd): void
{
$pedido = $this->pedidos->porId($cmd->pedidoId)
?? throw new PedidoNoEncontrado();
$pedido->confirmar(); // regla de negocio, dominio puro
$this->pasarela->cobrar($pedido->total());
$this->pedidos->guardar($pedido);
}
}
// Infraestructura — el ADAPTADOR. Aquí y SOLO aquí vive Doctrine.
namespace App\Infraestructura\Persistencia;
final class RepositorioPedidosDoctrine implements RepositorioPedidos
{
public function __construct(private EntityManagerInterface $em) {}
public function guardar(Pedido $pedido): void { $this->em->persist($pedido); $this->em->flush(); }
public function porId(PedidoId $id): ?Pedido { return $this->em->find(Pedido::class, $id); }
}
Lo que compras con esto es enorme:
- El dominio es testeable sin infraestructura. En los tests inyectas un
RepositorioPedidosEnMemoria: nada de base de datos, tests que vuelan. - La infraestructura es reemplazable. Cambiar de Stripe a Redsys es escribir un adaptador nuevo que implemente
PasarelaPago. El dominio no se entera. Vuelve al drama de las "40 clases con Stripe" del principio: con hexagonal, Stripe vive en un solo sitio. - El dominio grita de qué va el sistema. Abres
App\Dominioy vesPedido,Reserva,Tarifa—no ves Doctrine ni Symfony—. La herramienta es un detalle empujado a los bordes.
Conexión con DDD
Hexagonal es la carcasa arquitectónica natural de DDD. DDD te dice qué poner en el centro (el modelo de dominio rico, con su lenguaje ubicuo) y hexagonal te dice cómo protegerlo de que la infraestructura se le meta dentro. Los agregados y servicios de dominio de DDD viven en el núcleo del hexágono; los repositorios de DDD son precisamente los puertos de salida. Sin hexagonal (o algo equivalente), tu bonito modelo de dominio acaba contaminado de anotaciones de ORM y llamadas a servicios externos, y deja de ser dominio para ser "otra capa de la base de datos".
5. Clean Architecture: la regla de la dependencia¶
Clean Architecture (Robert C. Martin) es, en esencia, la misma idea que hexagonal, dibujada como círculos concéntricos y con una única regla que lo gobierna todo:
graph TB
subgraph C4["Frameworks y drivers (Symfony, Doctrine, web)"]
subgraph C3["Adaptadores de interfaz (controladores, repos)"]
subgraph C2["Casos de uso (lógica de aplicación)"]
subgraph C1["Entidades (reglas de negocio de empresa)"]
NUCLEO["DOMINIO"]
end
end
end
end
La regla de la dependencia: el código fuente de las dependencias solo puede apuntar hacia dentro. Nada de un círculo interior puede saber nada de un círculo exterior. Una entidad no sabe qué caso de uso la usa; un caso de uso no sabe qué controlador lo llama; el dominio no sabe que existe Symfony.
Cuanto más al centro, más estable, más general y más valioso: las entidades y reglas de negocio, que cambian poco y sobrevivirán a tres reescrituras de la capa web. Cuanto más afuera, más volátil y más reemplazable: frameworks, la web, la base de datos —lo que Martin llama, sin cariño, "detalles".
La idea que tienes que interiorizar es esta: las cosas estables no deben depender de las cosas volátiles. Si tu regla de negocio (estable, dura décadas) depende de tu framework (volátil, lo cambias en 5 años), has atado lo valioso a lo desechable. La regla de la dependencia invierte eso: lo volátil depende de lo estable, y nunca al revés.
Hexagonal y Clean no son rivales: son la misma intuición ("protege el dominio, invierte las dependencias hacia dentro") contada con distinta metáfora. No te obsesiones con cuántas capas o cuántos círculos: obsesiónate con la dirección de las flechas.
6. Monolito vs microservicios: tradeoffs reales, no moda¶
Aquí es donde más gente se estrella por seguir la moda. Vamos a los tradeoffs de verdad, sin hype.
Un monolito es una sola aplicación desplegable: todo el código en un proceso (o unos pocos idénticos tras un balanceador). Microservicios es partir el sistema en muchos servicios pequeños, cada uno desplegable por separado, comunicándose por la red.
El error de marketing es creer que microservicios es "la versión adulta" de un monolito. No lo es. Es un tradeoff con costes brutales que solo compensan bajo ciertas condiciones. Lo que realmente cambia al partir:
| Monolito | Microservicios | |
|---|---|---|
| Llamada entre módulos | Llamada a función: nanosegundos, no falla | Llamada de red: milisegundos, puede fallar |
| Transacciones | BEGIN … COMMIT: atomicidad gratis |
No hay transacción distribuida sencilla; necesitas sagas, consistencia eventual |
| Despliegue | Uno. Simple. | Decenas, coordinados, versionados |
| Depurar un fallo | Un stack trace | Rastrear una petición por 6 servicios (tracing distribuido) |
| Escalar | Escalas todo junto | Escalas solo la parte que lo necesita |
| Equipos | Todos tocan el mismo repo (fricción) | Cada equipo, su servicio (autonomía) |
| Complejidad | En el código | En la operación (red, despliegue, observabilidad) |
Los tres costes que la gente ignora hasta que sangran:
- La red no es fiable ni gratis. En un monolito, llamar a
facturacion->cobrar()es instantáneo y no falla. Entre servicios, esa misma llamada viaja por la red: tiene latencia (sumas milisegundos por salto, y una petición de usuario puede encadenar diez), puede fallar a medias, y te obliga a manejar reintentos, timeouts y circuit breakers. Convertiste una llamada a función en un problema de sistemas distribuidos. - Las transacciones dejan de ser fáciles. "Descuenta stock Y cobra Y crea el envío" en un monolito es una transacción de base de datos: o todo o nada. Repartido en tres servicios, no existe un
COMMITque abarque los tres. Tienes que orquestar sagas con compensaciones ("si el cobro falla, deshaz el descuento de stock"), y aceptar consistencia eventual (durante un rato, el mundo está inconsistente). Esto es de lo más difícil de programar bien que hay. - La complejidad no desaparece, se muda. Pasas complejidad del código a la operación: ahora necesitas descubrimiento de servicios, tracing distribuido, gestión de versiones de API, despliegues coordinados, y un equipo de plataforma que lo sostenga. Si no tienes esa madurez operativa, los microservicios no te dan agilidad: te dan un monolito distribuido, que es lo peor de los dos mundos —el acoplamiento de un monolito con la latencia y los fallos de red de los microservicios—.
El criterio honesto para partir (y por qué casi siempre es 'todavía no')
Los microservicios resuelven sobre todo un problema organizativo, no técnico: permiten que muchos equipos desplieguen sin pisarse. Su beneficio escala con el número de personas, no con lo molón de la arquitectura. Regla práctica: empieza por un monolito bien modularizado (con límites internos claros, al estilo hexagonal). Si algún día un módulo concreto necesita escalar aparte, o un equipo necesita despegarse, extrae ese servicio. Partir desde el día uno "porque es lo moderno" es, en el 90% de los casos, pagar toda la factura de complejidad distribuida sin ninguna de las ventajas —que solo llegan con escala de gente que aún no tienes—. El monolito modular no es la opción de principiante; muchas veces es la decisión de más nivel.
7. Atributos de calidad: por qué no puedes tenerlo todo¶
La arquitectura no se juzga por si "funciona" (eso es el código), sino por sus atributos de calidad: mantenibilidad, escalabilidad, testabilidad, rendimiento, seguridad, disponibilidad… Lo crucial, y lo que convierte esto en un oficio de tradeoffs:
Los atributos de calidad compiten entre sí. Casi nunca puedes maximizar uno sin sacrificar otro. La arquitectura es el arte de elegir qué atributos priorizas para este sistema.
Ejemplos de la tensión real:
- Rendimiento vs mantenibilidad: la solución más rápida suele ser un mazacote optimizado a mano, difícil de leer y tocar. El código más mantenible (capas limpias, indirección, puertos) mete saltos y abstracción que cuestan ciclos. Cada capa que añades por limpieza es un poquito de latencia.
- Escalabilidad vs simplicidad: una caché distribuida, colas y réplicas te dan escala, pero multiplican las piezas que pueden fallar y que hay que operar. Más escalable = más complejo de razonar.
- Seguridad vs usabilidad/rendimiento: cifrar todo, validar todo, aislar todo —protege, pero ralentiza y complica.
- Flexibilidad vs simplicidad: cada punto de extensión "por si acaso" (un puerto, un plugin, una config) es potencia futura pagada con complejidad presente. La mayoría no se usan nunca (esto es YAGNI).
Por eso no existe "la mejor arquitectura", solo "la mejor arquitectura para estos atributos priorizados en este contexto". Un banco prioriza consistencia y seguridad sobre latencia; una red social hace lo contrario. La misma decisión (p. ej. consistencia eventual) es acertada en una y catastrófica en el otro. Tu trabajo como arquitecto es saber qué estás priorizando y qué estás sacrificando, y poder defenderlo. Un diseño que "es bueno en todo" casi siempre significa que no has entendido los tradeoffs todavía.
8. Complejidad esencial vs accidental¶
Fred Brooks, en No Silver Bullet (1986), hizo la distinción que más te va a servir para pensar arquitectura:
- Complejidad esencial: la que es inherente al problema. Un sistema de reservas de vuelos con tarifas, overbooking, cambios y cancelaciones es intrínsecamente complicado. Esa complejidad no la puedes eliminar, solo modelarla bien; es el problema que te pagan por resolver.
- Complejidad accidental: la que añades tú (o tus herramientas, o tus decisiones) y que no venía en el problema. Un framework mal usado, cinco capas de abstracción que no hacían falta, una arquitectura de microservicios en un CRUD de tres tablas, configuración enrevesada, un ORM peleándose con tu modelo.
El objetivo de una buena arquitectura es absorber la complejidad esencial con la mínima complejidad accidental posible. La mayoría del sufrimiento en software es accidental —lo pusimos nosotros—.
La trampa mortal, y la razón de que esta lección insista tanto en "no por moda": las arquitecturas de moda son una fuente gigante de complejidad accidental cuando se aplican sin necesidad. Meter hexagonal, CQRS, event sourcing y microservicios en un blog personal no te hace mejor ingeniero; te hace añadir montañas de complejidad accidental a un problema que tenía complejidad esencial casi nula. La sofisticación de tu arquitectura debe igualar la complejidad esencial del problema, ni más ni menos. Sobre-arquitecturar es tan mal síntoma como no arquitecturar.
9. ADR: registrar por qué decidiste lo que decidiste¶
Las decisiones arquitectónicas son caras de cambiar; por tanto, dentro de un año alguien (quizá tú) va a mirar una y preguntarse "¿por qué narices lo hicimos así?". Si no está escrito el porqué, esa persona hará una de dos cosas malas: lo cambiará sin saber qué se rompe, o no lo tocará por miedo aunque debiera. Un ADR (Architecture Decision Record, registro de decisión arquitectónica) evita las dos.
Un ADR es un documento corto (una página) que captura una decisión importante. El formato clásico (Michael Nygard):
# ADR-007: Usar consistencia eventual entre Pedidos y Facturación
## Estado
Aceptada — 2026-03-12
## Contexto
Pedidos y Facturación van a vivir en servicios separados (equipos distintos).
No hay transacción distribuida barata entre ambos. Necesitamos decidir cómo
mantenemos la coherencia cuando se confirma un pedido.
## Decisión
Comunicación por eventos asíncronos ("PedidoConfirmado"). Facturación
reacciona y factura. Aceptamos una ventana de inconsistencia (segundos).
## Consecuencias
+ Servicios desacoplados, cada equipo despliega solo.
+ Resiliente: si Facturación cae, procesa el evento al volver.
- El usuario puede ver "pedido confirmado, factura pendiente" unos segundos.
- Necesitamos idempotencia y una cola fiable (coste operativo nuevo).
Lo valioso de un ADR no es la decisión, es el contexto y las consecuencias: registra qué sabíamos, qué alternativas descartamos y qué precio aceptamos pagar. Así, cuando el contexto cambie, quien venga podrá reevaluar con conocimiento de causa en vez de a ciegas. Los ADR se guardan en el propio repositorio (docs/adr/), versionados con el código, y son inmutables: si cambias de idea, escribes un ADR nuevo que "supera" al viejo, no borras el viejo. La historia de por qué el sistema es como es tiene tanto valor como el sistema.
Cuándo escribir un ADR
No documentes cada if. Escribe un ADR justo cuando dispares el "test de la reversibilidad" de la sección 1: la decisión es cara de cambiar y alguien va a preguntarse el porqué. Elección de base de datos, de estilo de comunicación entre servicios, de dónde vive una responsabilidad clave, de un tradeoff de consistencia. Si es barato de cambiar, no gastes un ADR: cámbialo y ya.
Cómo se ve en la práctica¶
Volvamos al drama del principio —"cambiar Stripe por Redsys y tocar 40 sitios"— y veámoslo con dos arquitecturas.
Sin frontera (dominio acoplado a la infraestructura). Stripe se llama directamente desde donde hace falta:
// ❌ En el controlador, en la entidad, en un listener de email, en un cron...
class PedidoController {
public function confirmar(Pedido $pedido) {
$stripe = new \Stripe\StripeClient($_ENV['STRIPE_KEY']);
$stripe->paymentIntents->create([ // Stripe metido AQUÍ
'amount' => $pedido->total() * 100,
'currency' => 'eur',
]);
}
}
Cambiar de pasarela = buscar Stripe por todo el proyecto y rezar. La lógica de negocio ("cobrar el total del pedido") está fundida con el detalle técnico ("cómo habla Stripe"). Es el acoplamiento arquitectónico de la sección 2, en carne viva.
Con un puerto (hexagonal). El dominio define qué necesita; la infraestructura lo cumple:
// Dominio: el puerto. Habla de negocio, no de Stripe.
interface PasarelaPago {
public function cobrar(Dinero $importe, ReferenciaPedido $ref): ResultadoPago;
}
// Caso de uso: depende del puerto. Le da igual quién cobre.
$this->pasarela->cobrar($pedido->total(), $pedido->referencia());
// Infraestructura: el adaptador. Stripe vive en UN solo archivo.
final class PasarelaStripe implements PasarelaPago { /* ... aquí y solo aquí ... */ }
Cambiar a Redsys = escribir PasarelaRedsys implements PasarelaPago y cambiar una línea de configuración de Symfony (qué adaptador se inyecta). Cero cambios en el dominio, cero miedo, tests del dominio intactos. La diferencia entre "tres semanas de arqueología" y "una tarde" no está en el código de Stripe: está en la decisión arquitectónica de haber puesto una frontera ahí. Eso es arquitectura gestionando el cambio.
Lo que sacrificas / errores comunes¶
- Confundir el diagrama con la arquitectura. Dibujar cajas y flechas bonitas no es arquitectura; la arquitectura real es la que revela
git log(qué cambia junto) y el coste real de revertir decisiones. Un diagrama que nadie respeta en el código es decoración. - Aplicar patrones por moda (astronauta de la arquitectura). Meter hexagonal + CQRS + microservicios en un CRUD sencillo es cargar toneladas de complejidad accidental sobre un problema con complejidad esencial mínima. La arquitectura debe igualar la complejidad del problema, no la moda del blog que leíste.
- Sobre-arquitecturar "por si acaso" (violar YAGNI). Cada puerto, capa y punto de extensión "para el futuro" es complejidad presente pagada por flexibilidad que casi nunca usarás. Añade fronteras cuando el dolor las pida, no antes.
- Creer que microservicios son la evolución natural. No son "mejores": son un tradeoff que resuelve un problema de escala organizativa a cambio de una brutal complejidad operativa. Sin muchos equipos y madurez operativa, te dan un monolito distribuido (lo peor de ambos mundos).
- Poner la base de datos en el centro. El error de las capas clásicas: atar el dominio (valioso, estable) a la infraestructura (detalle, volátil). Invierte las flechas: que el detalle dependa del dominio.
- No registrar los porqués. Sin ADRs, las decisiones caras de cambiar se vuelven cajas negras que nadie osa tocar o que alguien rompe sin saberlo. El contexto de una decisión vale tanto como la decisión.
- Buscar "la mejor arquitectura". No existe en abstracto; solo existe la mejor para estos atributos de calidad priorizados en este contexto. Si crees que tu diseño es bueno en todo, aún no has visto qué estás sacrificando.
Resumen¶
- La arquitectura es el conjunto de decisiones importantes y difíciles de cambiar —lo estructural, lo que queda cuando quitas los detalles—. Si algo es barato de revertir, no es arquitectura; decídelo rápido y sigue.
- Es acoplamiento y cohesión a nivel de sistema: junta lo que cambia junto, separa lo que cambia por razones distintas, y coloca las fronteras donde casi nunca tengas que cruzarlas para hacer un cambio.
- Hexagonal y Clean son la misma idea: pon el dominio en el centro, invierte las dependencias hacia dentro (puertos que define el dominio, adaptadores que implementa la infraestructura) para aislar lo valioso de lo volátil. Es la carcasa natural de DDD.
- Monolito vs microservicios es un tradeoff, no una evolución: partir cambia llamadas a función por llamadas de red (latencia, fallos), regala las transacciones fáciles y muda la complejidad del código a la operación. Compensa por escala de equipos, no por moda.
- Toda arquitectura es gestión de tradeoffs: los atributos de calidad compiten, la complejidad esencial es irreducible pero la accidental te la ahorras, y registrar los porqués (ADR) mantiene vivas las decisiones caras. La arquitectura no es un diagrama de moda: es gestionar el cambio.
Ejercicios socráticos¶
No busques la respuesta fuera. Razónala con el modelo de esta lección. Si te atascas, la pista está en el texto.
- Te dicen: "cambiamos de Symfony a Laravel". En un proyecto es una pesadilla de meses; en otro, una molestia acotada. Sin ver el código, ¿qué decisión arquitectónica distingue a los dos proyectos? ¿Dónde estaba, en cada uno, la dependencia del framework?
- Un compañero propone arrancar el nuevo producto (equipo de 4 personas, aún buscando encaje de mercado) directamente con 12 microservicios "para que escale desde el día uno". Da la objeción más fuerte que puedas usando tres conceptos de esta lección. ¿Qué le recomendarías y por qué?
- En la arquitectura en capas clásica se dice que "la base de datos está en el centro" y en hexagonal que "el dominio está en el centro". No es un cambio de dibujo: ¿qué flecha de dependencia se invierte exactamente, y por qué esa inversión es la que te permite cambiar el ORM sin tocar el negocio?
- Alguien defiende que su diseño "es el mejor: es rapidísimo, súper mantenible, ultra escalable y muy seguro, todo a la vez". Con lo que sabes de atributos de calidad, ¿por qué esa frase es una señal de alarma? ¿Qué le preguntarías para descubrir qué está sacrificando sin saberlo?
- Miras el
git logde un módulo supuestamente "independiente" y ves que sus ficheros cambian siempre en el mismo commit que los del módulo de facturación. El diagrama los pinta separados. ¿Cuál es tu arquitectura real, la del diagrama o la del historial? ¿Qué te dice eso sobre dónde deberías (o no) trazar una frontera de microservicio?
Repaso espaciado¶
Pasa estas a Anki (
repasos.md). Formato: pregunta que obligue a razonar, no a reconocer.
- [ ] Da la definición de "arquitectura" en términos de coste de cambio, y explica por qué "qué framework uso" normalmente no es una decisión arquitectónica.
- [ ] En puertos y adaptadores, ¿quién define el puerto y quién lo implementa? ¿Por qué esa dirección de dependencia (y no la contraria) es la que aísla el dominio de la infraestructura?
- [ ] Enumera los tres costes reales de pasar de monolito a microservicios (red, transacciones, operación) y explica por qué el beneficio escala con el número de equipos, no con lo moderno que sea.
- [ ] Distingue complejidad esencial de accidental con un ejemplo de cada una. ¿Por qué "aplicar arquitecturas de moda" es una fábrica de complejidad accidental?
- [ ] ¿Qué captura un ADR que el código por sí solo no captura, y por qué eso importa cuando una decisión es cara de cambiar?
Para seguir tirando del hilo¶
- Clean Architecture (Robert C. Martin) — la regla de la dependencia y los círculos concéntricos, explicados a fondo.
- No Silver Bullet (Fred Brooks, 1986) — el ensayo original sobre complejidad esencial vs accidental; corto y fundacional.
- "Hexagonal Architecture" (Alistair Cockburn) — el artículo original de puertos y adaptadores.
- Building Microservices (Sam Newman) — el libro honesto sobre microservicios: cuándo sí, cuándo no, y todo lo que cuesta.
- Fundamentals of Software Architecture (Richards & Ford) — atributos de calidad y estilos arquitectónicos con criterio de tradeoffs.
- Michael Nygard, "Documenting Architecture Decisions" — el post que popularizó los ADR, con el formato que copiar.