El Modelo Mental de Arquitectura para Servicios Node.js
La arquitectura, despojada de diagramas y nombres de frameworks, es simplemente el conjunto de decisiones en un sistema que son costosas de revertir. Dónde se ubican los límites de los módulos, en qué dirección apuntan las dependencias, si dos características comparten una tabla de base de datos, estas decisiones perduran más que cualquier función o ruta individual, y equivocarse en ellas no se manifiesta como un error, sino que aparece meses después como "¿por qué cada cambio en esta base de código tarda tres veces más de lo que debería?".
La arquitectura es la estructura duradera de un sistema (sus límites y direcciones de dependencia), evaluada por una pregunta práctica: ¿cuán costoso es cambiar una parte sin romper, o incluso comprender, el resto?
Por qué es importante: Una mala arquitectura no falla ruidosamente como un error, sino que se acumula silenciosamente como una entrega más lenta, hasta que un equipo se da cuenta de que cada característica ahora toca cinco archivos no relacionados.
Conceptos clave:acoplamiento, cohesión, límite, dirección de dependencia, inversión de dependencia, coste del cambio.
Cuándo usarlo: Al elegir dónde debe ir un límite de módulo, decidir si una dependencia pertenece a tu código de dominio o solo a tu infraestructura, evaluar si una división propuesta (servicio, capa, módulo) está resolviendo un problema real, o escribir un ADR que debe sobrevivir a la partida de la persona que lo escribió.
Limitaciones / Compromisos: Cada elección arquitectónica que reduce el acoplamiento añade indirección en algún otro lugar (interfaces, puertos, saltos adicionales), y esa indirección tiene un coste real en el código que tienes que leer para entender lo que realmente sucede.
Temas relacionados: monolitos modulares, arquitectura hexagonal (puertos y adaptadores), límites de microservicios, registros de decisiones de arquitectura.
Dos propiedades determinan casi todo sobre si una base de código sigue siendo fácil de cambiar: el acoplamiento y la cohesión. El acoplamiento es cuánto una parte de un sistema conoce o depende de los internos de otra parte; dos módulos están fuertemente acoplados si cambiar uno fuerza rutinariamente un cambio en el otro, incluso cuando la capacidad de negocio subyacente no cambió. La cohesión es cuánto las cosas dentro de una parte realmente pertenecen juntas; un módulo es altamente cohesivo si todo en él existe para servir una responsabilidad clara, y de baja cohesión si es un revoltijo de lógica no relacionada que terminó en el mismo archivo.
El objetivo que persigue la arquitectura es simple de enunciar y difícil de lograr: alta cohesión dentro de un límite, bajo acoplamiento entre límites. Un límite es cualquier línea que dibujas y haces cumplir (una carpeta, la API pública de un módulo, una llamada de red entre servicios) más allá de la cual el resto del sistema solo puede interactuar a través de una superficie explícita y estrecha en lugar de acceder directamente a los internos.
Una analogía útil: piensa en un sistema bien arquitectado como un edificio con muros de carga en los lugares correctos. Puedes renovar una cocina sin que se caiga el dormitorio, porque el muro entre ellos es un límite real, no una sugerencia. Un sistema mal arquitectado es el equivalente a que los muebles de cada habitación también funcionen como soporte estructural para la habitación de al lado; un solo cambio en cualquier lugar corre el riesgo de un colapso en todas partes.
// Acoplamiento fuerte: la función de dominio accede directamente a la infraestructuraimport { pool } from "../db/pool"; // el dominio ahora depende de la existencia de Postgresexport function createOrder(input: OrderInput) { return pool.query("INSERT INTO orders ...", [input]); // no se puede probar sin una base de datos real}
La dirección de la dependencia es donde esto deja de ser una preferencia abstracta y comienza a ser una regla concreta y verificable. Cada declaración de importación es una flecha de dependencia, y la dirección en que apuntan esas flechas determina qué partes de un sistema pueden cambiar independientemente de otras. Si tu lógica de dominio importa tipos de Express o un cliente de Prisma directamente, entonces la lógica de dominio ahora depende de esas elecciones; intercambiar frameworks u ORMs significa tocar reglas de negocio, no solo código de infraestructura.
La inversión de dependencia es la técnica específica que rompe esto: en lugar de que el dominio dependa de una implementación de infraestructura concreta, el dominio define una interfaz (un puerto) que describe lo que necesita, y el código de infraestructura (un adaptador) implementa esa interfaz para satisfacerlo. La flecha de dependencia ahora apunta hacia el dominio, no lejos de él; la infraestructura conoce el puerto del dominio, pero el dominio no sabe nada sobre Postgres, Express o Redis. Arquitectura Hexagonal en Node es exactamente este patrón, sistematizado con un diseño de carpetas concreto.
// El dominio define lo que necesita: un puerto, no un cliente de base de datosexport interface OrderRepository { save(order: Order): Promise<void>;}// createOrder depende del puerto, nunca de Postgres, Prisma o pg directamenteexport function createOrder(input: OrderInput, repo: OrderRepository) { const order = { id: crypto.randomUUID(), ...input }; return repo.save(order); // la infraestructura cumple este contrato más tarde}
Esta es la razón por la que "cambiar Express por Fastify" o "probar sin una base de datos real" se vuelve barato en un sistema bien delimitado y costoso en uno fuertemente acoplado: el código de dominio en la versión invertida nunca mencionó la tecnología concreta en primer lugar, por lo que reemplazarla solo toca el adaptador, nunca la lógica que realmente codifica las reglas de negocio.
Los límites también interactúan con la estructura del equipo de una manera que es fácil de subestimar. El límite de un módulo no es solo una costura técnica, sino que generalmente también es una costura de propiedad. Cuando la API pública de un módulo es estrecha y se aplica (a través de exportaciones de index.ts, restricciones de importación de ESLint o un límite de red real), un equipo puede cambiar los internos de ese módulo sin coordinarse con todos los demás equipos que tocan la base de código. Los límites débiles fuerzan la coordinación incluso cuando las capacidades de negocio subyacentes no dependen realmente entre sí, lo que a menudo es el coste real y palpable de "esta base de código es difícil de trabajar a medida que hemos crecido".
Los estilos arquitectónicos cubiertos en otras partes de esta sección (monolito en capas, monolito modular, hexagonal, microservicios) no son realmente filosofías diferentes que compiten por la respuesta "correcta". Son el mismo objetivo de acoplamiento/cohesión perseguido con diferentes grados de aplicación y diferentes costes operativos, y elegir entre ellos es un compromiso, no una escalera de madurez que todos deberían subir.
Un monolito en capas (carpetas globales controllers/, services/, models/) tiene límites solo por convención; nada impide que un controlador acceda directamente al modelo de otra característica, por lo que el acoplamiento se introduce a medida que la base de código crece a menos que se mantenga la disciplina. Un monolito modular mantiene un único desplegable pero aplica límites entre los módulos de características, típicamente a través de reglas de linting que bloquean las importaciones profundas; obtiene la mayoría de los beneficios de acoplamiento de los límites de servicio sin pagar por una red. La arquitectura hexagonal añade un segundo eje de límite, entre la lógica de dominio y cualquier infraestructura, independientemente del módulo, por lo que se compone naturalmente dentro de los módulos de un monolito modular en lugar de reemplazar la idea. Los microservicios convierten el límite del módulo en un límite de red genuino, aplicado por el sistema operativo y el protocolo de cable en lugar de un linter, que es la aplicación más fuerte posible, y también la más costosa, intercambiando llamadas a funciones en proceso por modos de fallo distribuidos, consistencia eventual y una superficie operativa mucho mayor.
Esa progresión se mapea directamente al coste del cambio, la lente práctica para evaluar cualquiera de estas opciones: cada paso hacia límites más fuertes reduce el coste de cambiar una parte de forma aislada y aumenta el coste de cambiar algo que legítimamente abarca dos partes, además del coste operativo fijo del propio mecanismo de límite. Un equipo que aún no ha sentido un dolor de acoplamiento real suele pagar una sobrecarga pura por una subestrategia de límites más fuerte de lo que necesita; un equipo que se ahoga en roturas entre módulos suele pagar una sobrecarga pura al dejar los límites solo por convención. Microservicios Cuando Vale la Pena y ADR: Monolito vs Servicios profundizan en la localización de ese punto de inflexión para un equipo específico.
Debido a que estas decisiones son costosas de revertir y se toman bajo una incertidumbre real, registrar el razonamiento en el momento de la decisión, no solo el resultado, importa más aquí que en casi cualquier otro lugar de una base de código. Un Registro de Decisiones de Arquitectura existe específicamente porque "por qué elegimos esto" es exactamente la información que se evapora más rápido de la memoria colectiva de un equipo, y volver a litigar un compromiso establecido sin conocer el contexto original desperdicia el esfuerzo exacto que la decisión original debía ahorrar.
Estilo
Fortaleza
Debilidad
Mejor ajuste
Monolito en capas (solo por convención)
Más sencillo para empezar; cero sobrecarga de aplicación
Los límites se erosionan silenciosamente a medida que crece la base de código
Bases de código pequeñas, individuales o de un solo equipo, productos en fase inicial
Monolito modular (límites aplicados)
La mayoría de los beneficios de acoplamiento de los servicios, sin coste de red
El proceso/base de datos compartidos aún acoplan los despliegues y los dominios de fallo
Múltiples equipos, un producto, cadencia de despliegue que aún no fuerza una división
Hexagonal (puertos/adaptadores)
Lógica de dominio testeable e independiente del framework
Indirección adicional: interfaces para leer incluso en casos simples
Lógica de dominio que vale la pena proteger de la rotación de la infraestructura
Microservicios
Despliegues independientes, aislamiento de fallos, verdadera autonomía del equipo
Transacciones distribuidas, modos de fallo de red, sobrecarga operativa real
Escala probada, herramientas de plataforma implementadas, cadencia de despliegue genuinamente bloqueada
"Arquitectura significa elegir una forma de diagrama de antemano." La arquitectura es un conjunto continuo de decisiones de límites y dependencias, la mayoría de las cuales se revisan a medida que aparece el dolor real del acoplamiento, no un ejercicio de diagrama único terminado antes de escribir código.
"Más capas o más servicios es automáticamente una mejor estructura." Cada límite adicional tiene un coste real: más indirección para leer o más superficie de red para operar. Los límites solo se amortizan cuando realmente resuelven el dolor de acoplamiento que siente el equipo.
"Los microservicios son la versión madura de un monolito." Son un punto diferente en la misma curva de compromiso, no una mejora; un monolito modular puede estar significativamente mejor arquitectado, en el sentido de acoplamiento/cohesión, que un conjunto de microservicios mal delimitados.
"Si está dividido en archivos y carpetas, tiene límites." Una carpeta es un límite solo si algo lo impone; sin una regla de lint, una API pública explícita o un salto de red, nada impide que el código lo atraviese, y lo hará, bajo la presión de los plazos.
"La inversión de dependencia es una sobreingeniería para la mayoría de los proyectos de Node." Es excesivo para un script desechable, pero para la lógica de dominio que vale la pena proteger de la rotación del framework o la base de datos, la interfaz "extra" es lo que hace que las pruebas y futuras migraciones sean baratas en lugar de una reescritura.
¿Qué significa realmente "arquitectura", concretamente, para un servicio Node.js?
El conjunto de decisiones estructurales que son costosas de revertir más tarde: dónde se ubican los límites de los módulos, en qué dirección apuntan las dependencias y cuánto depende una parte del sistema de los internos de otra parte. Todo lo demás es código ordinario que es barato de cambiar.
¿Cuál es la diferencia práctica entre acoplamiento y cohesión?
El acoplamiento mide cuánto una parte depende de los internos de otra parte; un acoplamiento alto significa que cambiar una fuerza rutinariamente cambios en otros lugares. La cohesión mide si las cosas dentro de una parte realmente pertenecen juntas. El objetivo es una alta cohesión dentro de un límite y un bajo acoplamiento entre límites.
¿Qué hace que algo sea un "límite" real en lugar de solo una carpeta?
La aplicación. Una división de carpeta es un límite solo si algo impide que el código lo atraviese directamente: una regla de lint que bloquea las importaciones profundas, una API pública explícita (exportaciones de index.ts) o una llamada de red real. Sin aplicación, una carpeta es solo una sugerencia organizativa.
¿Cómo funciona realmente la dirección de la dependencia?
Cada importación es una flecha de dependencia. Si la lógica de dominio importa un cliente de infraestructura concreto (un controlador de base de datos, un tipo de framework), la flecha apunta del dominio a la infraestructura, acoplando las reglas de negocio a esa tecnología específica. La inversión de dependencia invierte esto: el dominio define una interfaz que necesita, y el código de infraestructura la implementa, por lo que la flecha apunta hacia el dominio.
¿Por qué la inversión de dependencia facilita las pruebas?
Debido a que el código de dominio nunca hace referencia a una base de datos o framework HTTP concreto, una prueba puede proporcionar una implementación en memoria de la misma interfaz en lugar de configurar una infraestructura real; la lógica de dominio no puede notar la diferencia, ya que solo dependía de la forma de la interfaz.
¿Es un monolito modular simplemente "microservicios sin la red"?
Conceptualmente cercano: toma prestada la idea de límites aplicados de los microservicios mientras mantiene un único desplegable y un único proceso, lo que evita por completo los modos de fallo de red y las transacciones distribuidas. Obtiene la mayoría de los beneficios de acoplamiento sin la mayoría de los costes operativos.
¿Cuándo un límite más fuerte (como una división de servicio completa) realmente se amortiza?
Cuando el coste del acoplamiento (lanzamientos bloqueados, sobrecarga de coordinación entre equipos, un error de un equipo que derriba una característica no relacionada) excede mediblemente el coste operativo fijo del mecanismo de límite más fuerte. La evidencia de ese dolor, no la ambición arquitectónica, es la señal para buscarlo.
¿Por qué las decisiones de arquitectura deben documentarse (ADR) más que los cambios de código típicos?
Porque son costosas de revertir y se toman bajo una incertidumbre genuina; el razonamiento, no solo el resultado, es la información que un equipo futuro necesita para evaluar si el compromiso original sigue siendo válido. Ese contexto se evapora de la memoria más rápido que casi cualquier otro tipo de decisión.
¿Una buena arquitectura significa que no hay acoplamiento en ninguna parte?
No, el acoplamiento cero no es alcanzable ni deseable; algunas partes de un sistema realmente necesitan interactuar. El objetivo es asegurarse de que el acoplamiento exista dentro de límites cohesivos donde sea barato, y se mantenga bajo entre límites donde sea costoso de desenredar más tarde.
¿Es un error iniciar un nuevo servicio Node con arquitectura hexagonal y límites de módulo estrictos desde el primer día?
No necesariamente incorrecto, pero es un compromiso de coste real: los límites fuertes añaden una indirección que pagas de inmediato, a cambio de un dolor de acoplamiento que quizás aún no hayas sentido. Muchos equipos comienzan de manera más simple y añaden aplicación a medida que la base de código y el equipo realmente crecen hasta necesitarlo.
¿Cómo sé si mi arquitectura ha empeorado silenciosamente con el tiempo?
Busca síntomas en lugar de diagramas: un cambio en una característica requiere rutinariamente tocar archivos en características no relacionadas, los ingenieros de incorporación no pueden predecir dónde debe vivir el nuevo código, o dos equipos se bloquean mutuamente los lanzamientos a pesar de trabajar en características aparentemente separadas. Cada uno es un acoplamiento que se manifiesta como fricción en la entrega en lugar de un error.