Loading home…
Loading home…
Loading article…
¿Cómo demuestro que el dinero se movió si el cliente reintenta tras un timeout? Me lo pregunté al imaginar otra vez una columna balance y un UPDATE. Esa semana escribí movimientos que no se borran.

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.

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.

Seis cajas en el diagrama. Seis tentaciones de microservicio. Casi caí otra vez hasta entender que el mapa completo no obliga a desplegarlo entero la primera semana, y que separar repos no separa el dominio.
La semana pasada, al cerrar modelar dinero en código, dejé una frase al aire que desde entonces no me saco de la cabeza: primero el sistema tiene que saber contar. Después mover.
Money ya estaba en el repo. Los tests pasaban. Centavos en bigint, sin floats, sin atajos.
Pensé que lo siguiente sería rápido.
Me puse a diseñar el ledger y apareció algo muy familiar: una columna balance en accounts y un UPDATE cada vez que alguien transfiere. Lo mismo que hice con prisa en un reto técnico meses atrás. Lo mismo que enseñan muchos tutoriales. Funciona en Postman. Aguanta la demo.
Hasta que me hice la pregunta que antes había esquivado:
Si el cliente reintenta tras un timeout, ¿cómo demuestro que el dinero se movió?
Volví a esas dos líneas del reto:
from.balance -= dto.amount;
to.balance += dto.amount;
Durante mucho tiempo no vi el problema. El número bajaba en una cuenta y subía en la otra. Listo.
Pero un balance que cambia en silencio no deja rastro. No puedes reconstruir qué pasó. No puedes auditar a las tres de la mañana cuando soporte pregunta por una transferencia que "tal vez" se hizo.
Ahí entendí que el ledger no era un UPDATE. Era otra cosa.
La tentación sigue siendo la misma: guardar el saldo en la fila de la cuenta y actualizarlo en cada operación.
| Enfoque | Por qué tentaba | Por qué lo descarté |
|---|---|---|
balance mutable |
Leer saldo es un SELECT simple |
Pierdes historia; un fallo a mitad deja dudas |
| Movimientos que no se borran | Más trabajo al inicio | Puedes reconstruir; el saldo sale de lo que pasó |
Un depósito es un credit. Una transferencia son dos movimientos: debit en origen, credit en destino. Misma transacción de base de datos. Mismo transferReference. Si uno de los dos falla, ninguno queda a medias.
No es contabilidad de partida doble completa todavía. Es el primer paso honesto dentro del mismo servicio: dos líneas en el ledger, un solo commit.
Curiosamente, no levanté otro microservicio.
Seguí en el mismo accounts-service de fintech-core-platform y añadí tres piezas que me dan más tranquilidad: depositar con Idempotency-Key, consultar saldo y transferir entre cuentas.
El depósito usa Money. Si el cliente reintenta con la misma clave, devuelvo el mismo resultado sin duplicar el crédito. Eso ya lo había planteado en el mapa del POST /transfer; aquí es la primera parte que lo cumple de verdad.
La transferencia valida saldo antes de escribir. Si no alcanza, InsufficientFundsError. Si alcanza, dos filas en ledger_entries:
const debit = LedgerEntry.debit({
accountId: fromId,
amount: command.amount,
transferReference,
});
const credit = LedgerEntry.credit({
accountId: toId,
amount: command.amount,
transferReference,
});
await this.ledger.appendEntries([debit, credit]);
El saldo no vive en accounts. Lo calculo:
SUM(CASE WHEN direction = 'credit' THEN amount_cents ELSE -amount_cents END)
En TypeScript sigue siendo Money. En PostgreSQL, bigint en centavos. El usuario ve un número; el sistema guarda hechos.
Si viste mi reto técnico anterior, puede que te preguntes dónde quedó el bus. Ahí, apenas movías dinero, algo salía a un topic. Fraude podía enterarse. Notificaciones podían armarse.
Aquí todavía no.
El POST /accounts/transfers escribe en PostgreSQL y responde en la misma petición. No hay productor, no hay consumidor, no hay Redpanda levantada en este hito.
No lo veo como un paso atrás. Lo veo como orden: primero un ledger confiable en una sola base de datos; después avisar al resto con eventos. Si publicas a Kafka antes de saber contar bien, solo amplificas el error.
El bus vuelve en los artículos que vienen, con outbox y todo lo que ya probé en el reto, pero esta vez sobre movimientos en los que puedes confiar.
pnpm install
docker compose -f infra/docker/docker-compose.yml up -d
cp apps/accounts-service/.env.example apps/accounts-service/.env
pnpm dev:accounts
Dos cuentas, un depósito, una transferencia:
curl -s -X POST http://localhost:3001/accounts \
-H "Content-Type: application/json" \
-d '{"ownerName":"Alice"}'
curl -s -X POST http://localhost:3001/accounts \
-H "Content-Type: application/json" \
-d '{"ownerName":"Bob"}'
curl -s -X POST http://localhost:3001/accounts/{ALICE_ID}/deposit \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dep-001" \
-d '{"amountCents":1000}'
curl -s -X POST http://localhost:3001/accounts/transfers \
-H "Content-Type: application/json" \
-d '{"fromAccountId":"{ALICE_ID}","toAccountId":"{BOB_ID}","amountCents":400}'
curl -s http://localhost:3001/accounts/{ALICE_ID}/balance
curl -s http://localhost:3001/accounts/{BOB_ID}/balance
Alice debería quedar en 600, Bob en 400. Repite el depósito con la misma Idempotency-Key y no duplica.
Cuando trabajas con dinero, el saldo es una opinión calculada. Los movimientos son lo que pasó.
Eso no cierra todavía el tema del timeout en el mapa de ocho capas. Falta estado explícito de transferencia, eventos, fraude en segundo plano. Pero sin movimientos que no se borren, el resto se construye sobre un agujero.
Primero conté con Money. Esta semana moví dinero dentro del mismo proceso y dejé rastro. Después vendrá el bus.
Serie: makingcode.dev/series/fintech-core-platform
Anterior: Modelar dinero en código
Código: github.com/AndresED/fintech-core-platform (commit 2a7f79e)