NestJS Dependency Injection en Banking: Lo que Nadie te Cuenta
Detrás de cada originación de crédito hay un grafo de dependencias que puede tumbar producción. Lo que aprendí moviendo CDT, Bre-B y consumer credit a NestJS.
Hay una frase que repito en cada onboarding de devs nuevos en el equipo: en un banco, una dependencia mal inyectada no rompe un endpoint, rompe un contrato con un proveedor externo a las 3 AM. Y en consumer credit origination, eso significa transacciones en Transbank que no se reconcilian, eventos Bre-B que nunca llegan al switch, y un SQS lleno de mensajes que el orquestador ya dio por muertos.
NestJS te vende la dependency injection como una feature "automática". Lo que no te dice es que, en sistemas financieros, el grafo de dependencias ES el modelo de riesgo operacional. Si no lo entiendes, estás escribiendo código que parece limpio y se comporta como una bomba de tiempo.
El mito del "scope por defecto"
Cuando arranqué con NestJS vienes de Spring o de Go, lo primero que asumes es que el contenedor maneja el ciclo de vida por ti. Y sí, lo hace. Pero hay una diferencia brutal entre singleton, request ytransient cuando estás lidiando con:
- Contexto transaccional de bases de datos (KYC, motor de riesgo)
- Correlation IDs que deben propagarse entre servicios
- Clientes HTTP con tokens que rotan cada hora (PSE, Bre-B)
- Y al menos un proveedor que decide caerse justo cuando más lo necesitas
La pregunta no es "¿qué scope uso?", sino ¿qué garantía de aislamiento me da este scope en un cierre de lote?. Esa pregunta cambia todo.
El caso real: cliente Bre-B con token rotativo
Tenemos un BreBClient que firma cada request. El token vive 3600 segundos. Si lo declaras singleton (lo "natural" en NestJS), ganas performance pero pierdes la ventana de refresh. Si lo declarasrequest, pagas el handshake cada vez. ¿La respuesta correcta? Depende. Y ahí es donde el junior se traba.
@Injectable({ scope: Scope.REQUEST })
export class BreBClient {
private token?: { value: string; expiresAt: number };
constructor(
private readonly http: HttpService,
@Inject('BREB_CONFIG') private readonly cfg: BreBConfig,
) {}
async send(payload: BreBTransfer): Promise<BreBResponse> {
await this.ensureFreshToken();
return firstValueFrom(
this.http.post(this.cfg.endpoint, payload, {
headers: { Authorization: `Bearer ${this.token!.value}` },
}),
);
}
private async ensureFreshToken(): Promise<void> {
// ... el problema que vamos a dejar abierto por ahora
}
}Esto compila, pasa los tests unitarios, y se rompe en producción cuando dos requests concurrentes pelean por el mismo token. ¿Por qué? Porquerequest scope aísla por request HTTP, pero no por dominio de negocio. En banking, dos originaciones simultáneas pueden ser del mismo cliente y compartir ventana de token.
Tres patrones que me cambiaron la vida
Después de dos incidentes graves (uno con CDT, otro con KYC digital) y countless war rooms, llegué a tres patrones no negociables:
- Factory providers por dominio — no por clase. Un
CreditOriginationProviderdistinto al deAccountOpeningProvider, aunque compartan el mismo cliente HTTP. - Tokens explícitos para todo lo que cruza un trust boundary— interfaces no,
@Inject('PSE_CLIENT')sí. La magia de TypeScript no sobrevive a un merge de tres equipos. - Refresher como actor externo, no interno — el token no se renueva "cuando vence", lo renueva un job dedicado que el cliente consulta. Esto evita el race condition clásico del doble refresh.
¿Y los hexagonal puristas? Tienen razón en la teoría, pero en producción con SQS, RabbitMQ y deadlines de la Superintendencia, necesitas algo que tu equipo de 12 personas entienda un lunes a las 8 AM. Y más que voy a dejar fuera de este post para no arruinar el siguiente.
Lo que este post no te va a enseñar (a propósito)
Podría cerrar acá con un checklist bonito. Pero te estaría mintiendo. Hay al menos dos cosas que merecen su propio artículo y que prefiero dejar abiertas:
¿Cómo se ve un grafo de dependencias cuando un orquestador de consumer credit llama a 7 microservicios en cadena, y uno de ellos se cae a mitad de transacción? ¿Y cómo decides qué scope tiene un
Saga?
Esas preguntas no tienen respuesta en un snippet. Tienen respuesta en tu próximo postmortem. Y ahí es donde NestJS deja de ser un framework y se convierte en lo que siempre fue: una herramienta para tomar decisiones de arquitectura que nadie te documentó.
Si quieres que la siguiente entrega sea sobre el patrón de refresher externo con el ejemplo completo de Bre-B, déjalo en los comentarios. Si no, la guardaré para cuando alguien me la pida a las 3 AM.
Compartir artículo
Escrito por
Oscar Vargas
Apasionado por el desarrollo web, las herramientas modernas y compartir lo que aprende en el camino. Más artículos abajo.
Siguiente artículo →
El Costo Oculto de useEffect: Por Qué tu App Sigue Re-renderizando en 2026
Artículos relacionados
Tu CQRS en banca tiene dos verdades y nadie las reconcilia
El comando pasó, el evento llegó a SQS y la pantalla mostró un saldo falso. El bug no estaba en el command side, sino en el lado que nadie mira.
El Provider de NestJS que Firmó un Contrato que tu Banco No Renovó
Tu @Injectable vive en el scope equivocado y el proveedor externo ya cambió las reglas. Lo que aprendí cuando PSE, Bre-B y Transbank dejaron de hablar con mi grafo.
Go te devuelve el error, pero el dinero ya se movió
Un error nil, una transferencia duplicada y 48 horas de conciliación rota. En Go el fallo no se lanza: se esconde. Te cuento dónde lo perdimos.