Dirección de dependencias e inversión de control | Nicolás Garzón
La dirección de las dependencias determina qué partes quedan condicionadas por otras. Las reglas de negocio deberían depender de conceptos estables propios, mientras frameworks, bases de datos y proveedores se conectan desde el exterior.
En una aplicación, el flujo de ejecución y la dirección de dependencia no tienen que apuntar en el mismo sentido.
Texto
Copiar flujo de ejecución:
caso de uso → repository → base de datos
dependencia de código:
adaptador de base → contrato definido por aplicaciónEsta inversión permite que el código de negocio controle el contrato que necesita sin importar detalles concretos.
Cuando un caso de uso importa directamente un ORM, SDK o framework:
Sus tipos externos invaden la lógica.
Las pruebas dependen de infraestructura.
Cambiar proveedor afecta reglas.
Los errores técnicos se filtran al dominio.
El framework termina organizando toda la aplicación.
TypeScript
Copiar import { prisma } from "../database" ;
import Stripe from "stripe" ;
export async function confirmOrder ( orderId: string ) {
const order = await prisma. order. findUnique ( { where: { id: orderId } } ) ;
} La operación conoce esquema, cliente y proveedor. Una modificación en cualquiera puede propagarse al flujo completo.
Los módulos de alto nivel no deberían depender de módulos de bajo nivel.
Ambos deberían depender de abstracciones.
Las abstracciones no deberían depender de detalles.
Los detalles deberían depender de abstracciones.
“Alto nivel” no significa una carpeta superior. Significa políticas o reglas con valor propio. “Bajo nivel” significa mecanismos: HTTP, PostgreSQL, filesystem o APIs externas.
El contrato debe expresar lo que necesita el caso de uso:
TypeScript
Copiar export interface OrderRepository {
findById ( id: OrderId) : Promise < Order | null > ;
save ( order: Order) : Promise < void > ;
} No debería copiar la API del ORM:
TypeScript
Copiar interface GenericRepository< T > {
findMany ( args: unknown ) : Promise < T [ ] > ;
update ( where: unknown , data: unknown ) : Promise < T > ;
} El primer contrato habla el lenguaje del consumidor y limita capacidades. El segundo filtra un mecanismo y permite saltarse invariantes.
Un puerto saliente representa una capacidad que la aplicación necesita:
Persistir un agregado.
Autorizar un pago.
Publicar un evento.
Obtener la hora.
Generar un identificador.
TypeScript
Copiar interface PaymentGateway {
authorize ( input: PaymentAuthorization) : Promise < AuthorizationResult> ;
} El puerto no promete que cualquier proveedor sea intercambiable sin costo. Protege al caso de uso de detalles que no debería conocer.
El código de aplicación no crea sus dependencias. El ensamblaje ocurre en un composition root:
TypeScript
Copiar const orderRepository = new PostgresOrderRepository ( db) ;
const paymentGateway = new StripePaymentGateway ( stripeClient) ;
const eventPublisher = new OutboxEventPublisher ( db) ;
const confirmOrder = new ConfirmOrder (
orderRepository,
paymentGateway,
eventPublisher,
) ; El composition root conoce implementaciones. El caso de uso conoce contratos.
TypeScript
Copiar class ConfirmOrder {
constructor (
private readonly orders: OrderRepository,
private readonly payments: PaymentGateway,
) { }
async execute ( command: ConfirmOrderCommand) : Promise < ConfirmOrderResult> {
const order = await this . orders. findById ( command. orderId) ;
if ( ! order) return { type: "not-found" } ;
if ( ! order. canBeConfirmed ( ) ) return { type: "invalid-state" } ;
const authorization = await this . payments. authorize ( {
amount: order. total,
paymentMethodId: command. paymentMethodId,
idempotencyKey: command. requestId,
} ) ;
if ( ! authorization. approved) return { type: "payment-rejected" } ;
order. confirm ( authorization. id) ;
await this . orders. save ( order) ;
return { type: "confirmed" , orderId: order. id } ;
}
}
El comando llega con identidad de la intención.
El repository devuelve un modelo de dominio o ausencia.
El agregado protege su transición de estado.
El gateway traduce la operación al proveedor.
El resultado externo se convierte en lenguaje de aplicación.
El repository persiste el nuevo estado.
En producción, autorizar y guardar sin coordinación puede producir un pago aprobado con persistencia fallida. El ejemplo enseña dirección de dependencias, no resuelve por sí solo atomicidad distribuida. Podrían necesitarse estados pendientes, saga, reconciliación o un diseño distinto.
Las partes más volátiles deberían depender de partes más estables.
Texto
Copiar UI cambia frecuentemente
→ depende de casos de uso
ORM cambia por decisión técnica
→ implementa puertos
dominio cambia por reglas de negocio
→ no depende de UI ni ORMEstabilidad no significa que el dominio nunca cambie. Significa que sus cambios deberían responder a negocio y no a mecanismos externos.
Protege una frontera arquitectónica.
Permite controlar una dependencia no determinista.
Traduce semántica externa.
Existen ritmos de cambio distintos.
Se necesita una implementación alternativa real.
Solo duplica una clase sin reducir conocimiento.
La dependencia es estable, local y trivial.
El contrato imita exactamente al proveedor.
Se diseña para implementaciones imaginarias.
Las dependencias aparecen en el constructor o función. El grafo es visible y las pruebas pueden suministrar alternativas.
El código busca dependencias globalmente:
TypeScript
Copiar const gateway = container. resolve ( "paymentGateway" ) ; Esto oculta requisitos, dificulta análisis y permite fallos en runtime. Un contenedor puede ser útil en el composition root, pero no debería dispersarse por casos de uso.
Un puerto falla cuando obliga al consumidor a conocer el proveedor:
TypeScript
Copiar interface PaymentGateway {
createStripePaymentIntent ( params: Stripe. PaymentIntentCreateParams) : Promise < Stripe. PaymentIntent> ;
} Aunque existe una interfaz, el acoplamiento permanece. El contrato correcto traduce tipos, errores, estados, unidades e idempotencia al lenguaje propio.
La infraestructura debe mapear errores técnicos:
Texto
Copiar unique constraint
→ OrderAlreadyExists
timeout del proveedor
→ PaymentResultUnknown
credencial inválida
→ PaymentProviderConfigurationErrorNo todos los errores deben convertirse en uno genérico. El consumidor necesita distinguir qué puede reintentar, compensar o mostrar.
Usa fakes o stubs de puertos para probar reglas y resultados.
Prueba integración con base, sandbox o emulador real.
Verifica que implementaciones respeten semántica, no solo firmas.
Un fake rápido no demuestra que el adapter real maneje transacciones o errores correctamente.
Una interfaz puede tener Stripe y Mercado Pago, pero no fuerces un mínimo común que elimine capacidades necesarias. Usa contratos por capacidad.
La inversión de dependencia no define por sí sola dónde comienza la transacción. Un Unit of Work o puerto transaccional puede coordinar adapters locales.
En aplicaciones sencillas, abstraer cada API del framework puede ser innecesario. Protege únicamente decisiones cuyo cambio o prueba importa.
Una abstracción local casi no añade costo de runtime, pero sí costo cognitivo. Evita capas de forwarding sin decisión.
Una interfaz por cada clase.
Repositories genéricos.
Service locator global.
Puertos definidos desde la implementación.
DTOs externos atravesando el dominio.
Ocultar el ORM sin proteger invariantes.
Creer que DI container equivale a inversión de dependencias.
Flujo de ejecución y dependencia de código pueden apuntar en sentidos distintos.
El consumidor debe definir el contrato que necesita.
El composition root ensambla implementaciones.
Una interfaz solo aporta si reduce conocimiento o protege una frontera.
Los adapters traducen semántica y errores, no solo nombres.
La inversión de dependencias mejora testabilidad, pero no resuelve automáticamente transacciones ni fallos distribuidos.
¿Por qué un repository genérico puede debilitar el dominio?
¿Dónde debería usarse un contenedor de dependencias?
¿Qué diferencia existe entre inyección de dependencias e inversión de dependencias?
¿Qué debería traducir un adapter de pagos?
¿Por qué el ejemplo de ConfirmOrder todavía tiene un riesgo distribuido?
Límites, contratos y ownership amplía la dirección de dependencias hacia la colaboración entre módulos, procesos y equipos.