Loading home…
Loading home…
Loading article…
¿Quién más se entera cuando el dinero ya se movió? Me lo pregunté con el ledger recién listo y sin ningún topic activo. Esa semana separé lo que pasó en el dominio del mensaje que saldrá a Kafka.

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.
Al cerrar la publicación del ledger interno, dejé una frase al aire que desde entonces no me saco de la cabeza: después vendrá el bus.
Esa frase hablaba de lo que pasa cuando el dinero ya se movió. En mi reto técnico anterior eso significaba Kafka: apenas completabas una transferencia, algo salía a un topic. Fraude se enteraba. Notificaciones se armaban. Otro equipo podía reaccionar sin meterse en cómo guardabas las cuentas.
Aquí el ledger ya había cerrado otra parte. Depósito, transferencia, saldo derivado. Los tests pasaban. Todo en la misma petición, todo en PostgreSQL.
Pensé que lo siguiente salía casi solo: importar kafkajs, repetir el guion y levantar el bus.
Casi lo hice. Por inercia.
Pero antes de publicar nada me frenó una pregunta que el ledger no contesta. El ledger te dice si contaste bien. No te dice a quién hay que avisar ni con qué palabras.
Y avisar a alguien implica decidir qué le cuentas. Ahí recordé un error que había visto muchas veces: confundir lo que pasó adentro con lo que hay que mandar afuera.
Lo primero que se me ocurrió fue lo más rápido: tomar la fila que acababa de guardar en TypeORM, serializarla y publicarla en Kafka. En muchos equipos eso se vende como "evento de dominio". En la demo funciona. Pasas los tests. El topic recibe algo.
El problema llega meses después. Renombras una columna para acomodar el ledger, o cambias cómo guardas la clave de idempotencia, y de pronto el servicio de fraude deja de entender el mensaje. O el de notificaciones. O el que todavía no existía cuando escribiste el producer. El acoplamiento no vino de Kafka: vino de mezclar dos preguntas distintas en un solo JSON.
La primera pregunta es de negocio: ¿qué pasó en accounts? Ahí necesito nombres que entienda quien trabaja el dominio, como FundsDeposited o InternalTransferCompleted. Son hechos que ya ocurrieron dentro del módulo.
La segunda es de contrato: ¿qué le prometo al resto del sistema? Ahí necesito un mensaje versionado y estable, sin detalles de ORM, como com.fintech.account.credited.v1. Fraude, notificaciones u otro servicio pueden consumirlo sin saber cómo guardo las filas en PostgreSQL.
Miré las dos opciones con la misma lógica que usé al elegir Money sobre float:
| Enfoque | Por qué tentaba | Por qué lo descarté |
|---|---|---|
| Publicar la entidad tal cual | Menos código el primer día | Acopla base de datos, dominio y contrato externo |
| Separar dominio e integración | Más tipos y un mapper | Cada capa habla su idioma; el contrato se versiona aparte |
Por eso no empecé por Kafka. Primero modelé el hecho adentro y el mensaje afuera. Cuando un depósito cierra, registro FundsDeposited dentro del servicio; un handler lo traduce a com.fintech.account.credited.v1 para quien esté fuera. Recién con esa traducción clara tiene sentido volver al bus que prometí al cerrar el artículo anterior. No al revés: no enciendo el bus para después descubrir qué debió haber salido.
Con esa distinción en la cabeza, el código de la semana fue armar el puente entre el ledger y el bus, todavía sin levantar Redpanda.
Curiosamente, no añadí otro servicio. Seguí en el mismo accounts-service de fintech-core-platform y cableé una cadena simple: primero se persiste el movimiento en PostgreSQL; después se publica el hecho de dominio; un handler lo traduce al contrato de integración; y, por ahora, un log en consola muestra el JSON que más adelante irá a fintech.accounts.events.
Un depósito muestra bien cómo se siente eso en la práctica. Cuando termina, y no es un reintento idempotente, el servicio registra el hecho adentro:
await this.domainEvents.publish(
new FundsDepositedEvent(
accountId,
command.amount,
command.idempotencyKey,
entry.id,
),
);
Ese hecho todavía no es el mensaje que verá fraude ni notificaciones. Un handler lo convierte en el contrato externo:
// Dominio → integración
com.fintech.account.credited.v1
{
accountId, amountCents, currency,
idempotencyKey, ledgerEntryId
}
La transferencia interna repite el mismo patrón: InternalTransferCompleted adentro, com.fintech.transfer.completed.v1 afuera, con transferReference, cuentas y monto.
Elegí dejar ese contrato en un log antes de conectarlo a Kafka. Si en un solo commit mezclo ledger, mapper y producer, y algo falla, no sé qué pieza falló. Primero quiero ver el JSON en consola, validar que dice lo que necesito, y recién después enchufar Redpanda con outbox, como en el reto anterior, pero esta vez sobre un ledger en el que ya puedo confiar.
Mismo setup que el artículo del ledger:
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
Haz un depósito y mira la consola del servicio. Si la cadena cerró bien, deberías ver algo así:
{
"message": "integration_event_ready",
"eventType": "com.fintech.account.credited.v1",
"eventId": "...",
"payload": { "accountId": "...", "amountCents": "1000", ... }
}
Ese JSON confirma las dos capas que separé arriba: el dinero quedó registrado en el ledger y el mensaje de integración ya tiene forma estable para quien consuma desde afuera. Si repites el depósito con la misma Idempotency-Key, el ledger no duplica y el reintento se corta antes de publicar otra vez.
En el reto anterior esa separación ya existía, solo que todo corría más deprisa. Fraude consumía eventos sin importarle cómo persistíamos las cuentas. Aquí la estoy escribiendo con más calma porque primero necesitaba el ledger del artículo pasado.
Un evento de dominio cuenta lo que ya ocurrió adentro. Un evento de integración es la versión que le muestras a quien está fuera. Sin esa frontera, el bus solo reparte acoplamiento más rápido.
Primero aprendí a contar con Money. Después moví dinero y dejé rastro. Esta semana supe qué avisar y en qué formato. Lo que falta es conectar Redpanda, no inventar el contrato mientras el bus ya está encendido.
Serie: makingcode.dev/series/fintech-core-platform
Código: github.com/AndresED/fintech-core-platform (commit e6146ef)