Loading home…
Loading home…
Loading article…
Tres segundos de spinner. Timeout en pantalla. ¿Se movió el dinero? Reabrí mi challenge de Arkano con un mapa de arquitectura fintech al lado y entendí por qué un POST /transfer nunca fue solo un endpoint.

Sin spam — solo un aviso cuando publique algo nuevo sobre backend, cloud y arquitectura.
Un email cuando salga un artículo. Puedes darte de baja cuando quieras.

Un post de LinkedIn sobre arquitectura fintech, un reto técnico reciente y la decisión de reconstruir un core bancario desde cero — más lento, para aprender de verdad.

Si agregar un nuevo cliente implica desplegar una nueva aplicación o copiar una base de datos completa, probablemente tu arquitectura SaaS no está preparada para escalar. Aprende cómo implementar multi-tenancy en NestJS de forma limpia y mantenible

Si cada nueva funcionalidad te obliga a modificar cinco servicios distintos, probablemente tengas un problema de acoplamiento. Aprende cómo Event-Driven Architecture ayuda a desacoplar módulos y escalar aplicaciones NestJS.
Tres segundos de spinner. Pantalla en blanco. La solicitud tardó demasiado.
El usuario no piensa en PostgreSQL ni en idempotency keys. Piensa en si su dinero volvió o se fue. Y tú, del otro lado, tienes que saberlo.
Cuando eso me pasó por primera vez no en producción, en una demo no supe responder. El endpoint había devuelto 200 otras veces. Pero esa vez la conexión se cortó antes de que el cliente leyera la respuesta. ¿Había transferido o no?
Esa pregunta es el hilo de este artículo.
En la introducción de la serie conté por qué vuelvo a un core bancario desde cero, sin prisa. La promesa era concreta: no otro sprint contra reloj, sino entender y explicar cada capa.
El primer paso no fue abrir NestJS. Fue reabrir viejas decisiones.
Cuando empecé esta serie, reabrí arkano-banking-challenge con un mapa de arquitectura fintech en otra pestaña gateway, auth, cuentas, fraude, Kafka, observabilidad. Hice scroll hasta transfers y me quedé mirando un método que yo había escrito hace unos meses.
Funcionaba. Había pasado la prueba. Pero al lado de ese mapa, mi POST /transfer contaba otra historia: la de alguien que optimizó para el check verde de Postman, no para la pregunta del timeout.
No lo digo con vergüenza. Un challenge de 48 horas premia entregar. Lo que no premia es explicar qué pasa cuando algo falla a mitad.
En la pantalla del usuario todo cabe en cuatro gestos:
Cuatro pasos. Simplicidad absoluta. Eso es producto: diseño, copy, la sensación de que "esto es fácil".
Yo, en Arkano, había diseñado para el paso 4. Casi no había pensado en los ocho pasos que pueden vivir entre el tap y la confirmación en una fintech que ya escala.
Una tarde, con un esquema de arquitectura fintech en una pantalla y un cuaderno en la otra, intenté traducir cada caja a una pregunta concreta. Esto es lo que salió:
| Paso | Componente | Qué resuelve | ¿Sync o async? |
|---|---|---|---|
| 1 | API Gateway | ¿Quién entra y a qué ritmo? (TLS, rate limit, WAF) | Sync |
| 2 | Auth | ¿Es esta persona y puede hacer esto? (JWT, scopes, MFA) | Sync |
| 3 | Accounts | ¿Hay saldo, cuenta activa, límites? | Sync |
| 4 | Transfers / Ledger | ¿Se mueve el dinero sin romper invariantes? | Sync (TX DB) |
| 5 | Fraud | ¿Este patrón es sospechoso? | Sync o async |
| 6 | Outbox + bus | ¿Quién más tiene que enterarse? | Async |
| 7 | Notifications | ¿Cómo le avisamos al usuario? | Async |
| 8 | Observability | ¿Podemos reconstruir qué pasó? | Async / sidecar |
La primera vez que leí la tabla entera pensé: ¿todo esto para mover $100 de una cuenta a otra?
La segunda vez entendí algo que muchos diagramas de fintech repiten sin decirlo en voz alta: la complejidad no desaparece. Solo cambia de lugar. El usuario no la ve porque alguien en algún momento decidió dónde vivía cada preocupación. El producto esconde la complejidad; ingeniería la ubica.
Y aquí viene el matiz que me costó años interiorizar: ningún paso es "relleno" en producción, pero no todos existen en la semana uno. Conocer el mapa entero y construir solo lo que la fase actual necesita eso es el oficio. No es YAGNI contra el diagrama; es YAGNI con el diagrama en la pared.
En entrevistas de system design te piden algo como esto. En producción, cada flecha tiene dueño, SLA y runbook. Yo no lo tenía cuando escribí mi primer endpoint solo la urgencia de que compilara:
Hoy lo miro y separo dos caminos mentalmente.
Camino que el usuario espera auth, saldo, mover dinero, respuesta. Si esto tarda más de unos segundos, hay ansiedad.
Camino que puede esperar email, push, métricas, auditoría async. Si el bus está lento, la transferencia ya está hecha; el usuario no debería enterarse por un spinner infinito.
En Arkano mezclé ambos en el mismo método. El email viajaba en el request path. La transferencia dependía del SMTP. Eso no lo vi el día del challenge; lo vi el día que comparé mi código con el diagrama.
Así se veía mi endpoint:
@Post('transfers')
async transfer(@Body() dto: TransferDto) {
const from = await this.repo.findOne(dto.fromAccountId);
if (from.balance < dto.amount) throw new BadRequestException();
from.balance -= dto.amount;
const to = await this.repo.findOne(dto.toAccountId);
to.balance += dto.amount;
await this.repo.save([from, to]);
await this.emailService.sendReceipt(dto.userEmail);
return { ok: true };
}
Recuerdo la satisfacción de verlo verde en Postman. Cuenta A menos, cuenta B más, email enviado, ok: true. Demo cerrada. Foto mental guardada.
Meses después, el mismo bloque se convirtió en una lista de preguntas que no podía responder con tranquilidad:
balance, ¿qué pasa con el centavo que se pierde en redondeo?save falla a mitad, ¿qué queda en disco sin transacción explícita?No es que el snippet esté mal para un challenge contra reloj. Está incompleto para el oficio que quiero practicar ahora. Y todas esas preguntas convergen en una sola escena.
Vuelve al inicio: el usuario pulsa Enviar. El spinner gira. Tres segundos. Timeout.
¿Se movió el dinero?
Con balance -= amount y return { ok: true }, no lo sabes. Quizá debitaste y el cliente nunca leyó el 200. Quizá falló antes del save y el usuario cree que envió. Quizá reintentó y cobraste dos veces.
En fintech, "no lo sé" no es una respuesta aceptable. No para soporte. No para una entrevista senior. No para dormir tranquilo si algún día esto toca dinero real.
Lo que necesitaba y lo que voy a construir en esta serie se resume en tres ideas:
Idempotency-Key devuelve la misma respuesta sin doble débito.PENDING | COMPLETED | FAILED, no solo como saldo mutado en silencio.Por eso el ledger entra en la Fase 1. No por postureo de microservicios. Porque sin esas tres piezas la pregunta del timeout sigue sin respuesta.
Cuando solo quería entregar rápido, el trade-off me confundía. Pensaba que había dos opciones: diagrama enterprise completo o CRUD y ya.
Con el tiempo entendí que hay una tercera la que esta serie intenta demostrar:
| Enfoque | Lo que ganas | Lo que arriesgas |
|---|---|---|
| Todo el diagrama día 1 | README impresionante | Entender poco de verdad |
| Solo CRUD sin mapa | Entregar rápido | Quedarte en blanco en entrevista |
| Mapa completo + build incremental | Aprendizaje honesto | Disciplina para no saltarte fases |
Complejidad oculta vs YAGNI no es elegir un bando. Es tener el mapa en la pared y poner en la mesa solo los ladrillos de hoy.
Para la Fase 0 de fintech-core-platform eso significa:
Idempotency-Key desde el primer endpoint real, OpenAPI como contrato.En Arkano prioricé el check verde. Aquí priorizo poder contar qué pasa en cada flecha del diagrama empezando por las que el usuario nunca ve.
Así se ve el código que quiero escribir ahora: controller delgado, lógica en el use case, nada de email ni Kafka en el camino que el usuario espera:
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Headers('idempotency-key') idempotencyKey: string,
@Body() dto: CreateTransferDto,
): Promise<TransferResponseDto> {
const result = await this.createTransfer.execute({
idempotencyKey,
fromAccountId: dto.fromAccountId,
toAccountId: dto.toAccountId,
amountCents: dto.amountCents,
});
return TransferResponseDto.from(result);
}
El use case orquesta dominio + puerto de persistencia. Tests unitarios sin Docker. Un ladrillo no el edificio entero.
El edificio se llena fase a fase: ledger en la 1, eventos en la 2, fraude cuando el flujo sync ya no mienta. Sin atajos. Sin README que prometa lo que el código aún no sostiene.
Mismo dominio que Arkano. Distinto ritmo. En el intro dije que corrí una vez; ahora camino la maratón. Este artículo es el primer kilómetro el que empieza cuando dejas de mirar solo el check verde y empiezas a mirar el mapa.
Serie: makingcode.dev/series/fintech-core-platform
Anterior: Por qué vuelvo a un core bancario