Loading home…
Loading home…
Loading article…
¿Qué pasa cuando el dinero deja de ser un número y se convierte en una responsabilidad? Me lo pregunté al ver amountCents en el borrador del POST /transfer. Ese día escribí Money.

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 el monolito modular, dejé el edificio con un departamento habitado.
fintech-core-platform tenía Fase 0 en el repo: un solo deploy, cuentas que ya se podían crear, la lógica de negocio separada del framework y del HTTP, tests que corrían sin levantar PostgreSQL. Al cerrar dije que transferencias y movimientos contables vendrían pronto, sin multiplicar servicios todavía. Pero antes, algo menos glamoroso: definir qué es el dinero en código. Centavos. bigint. Nada de float.
Creía que, una vez dicho eso, ya tocaba mover dinero. Abrí el borrador del endpoint de transferencias.
POST /transfer
Y me quedé mirando una sola propiedad:
{ "amountCents": 1050 }
Nada extraordinario. Solo un monto.
Pero esa propiedad me hizo detenerme.
Recordé algo que había escrito meses atrás en un reto técnico. Un proyecto que salió bien, pasó las pruebas, cumplió los requisitos y quedó publicado en GitHub. El saldo de una cuenta era simplemente esto:
from.balance -= dto.amount;
to.balance += dto.amount;
Durante mucho tiempo no vi ningún problema. Funcionaba.
Hasta que me hice una pregunta sencilla: ¿qué pasa cuando el dinero deja de ser un número y se convierte en una responsabilidad?
Ahí entendí que el problema no era el ledger. Ni la idempotencia. Ni los timeouts. Ni siquiera la consistencia entre servicios.
El problema era que todavía no había definido qué significaba el dinero dentro del sistema.
Un usuario ve $10.50. El sistema no debería ver un decimal. Debería ver 1050 centavos. Exactamente 1050. Ni uno más. Ni uno menos.
Cuando empecé a construir este core fintech desde cero, decidí que ese sería el primer concepto que modelaría bien. Antes incluso que las transferencias.
Miré las opciones que siempre aparecen en posts técnicos y que yo mismo había esquivado con prisa:
| Enfoque | Por qué tentaba | Por qué lo descarté |
|---|---|---|
float / number |
Rápido, nativo en JS | 0.1 + 0.2 no es 0.3 |
decimal en DB + string |
Suena "contable" | Parsing y fricción en TypeScript |
Centavos + bigint |
Menos cómodo al inicio | Enteros exactos, reglas en el código de negocio |
No elegí bigint porque vaya a mover billones mañana. Lo elegí porque quiero que sumar y restar dinero en este proyecto sea aritmética entera, no una apuesta.
El decimal de PostgreSQL llegará con el ledger. Este tipo no sabe nada de base de datos ni de JSON.
Curiosamente, ese día no escribí el ledger.
Terminé construyendo algo mucho más pequeño: una clase llamada Money.
No tiene endpoints. No tiene base de datos. No tiene eventos. No tiene infraestructura.
Pero probablemente sea una de las piezas más importantes de todo el proyecto.
Vive en @fintech/domain-common, la lib compartida del monorepo, porque Accounts, Transfers y el ledger van a hablar el mismo idioma:
export class Money {
private constructor(
readonly amountCents: bigint,
readonly currency: string,
) {}
static fromCents(amountCents: bigint, currency = 'USD'): Money {
if (amountCents < 0n) {
throw new NegativeMoneyError();
}
return new Money(amountCents, currency);
}
add(other: Money): Money {
this.assertSameCurrency(other);
return Money.fromCents(this.amountCents + other.amountCents, this.currency);
}
subtract(other: Money): Money {
this.assertSameCurrency(other);
if (other.amountCents > this.amountCents) {
throw new InsufficientFundsError();
}
return Money.fromCents(this.amountCents - other.amountCents, this.currency);
}
}
add y subtract devuelven otro Money. No mutan. Si mezclas monedas, el código falla con un error claro en lugar de sumar mal en silencio.
Los tests corren sin Docker. Dejé uno que parece obvio y no lo es:
it('should document that JS float arithmetic is unsafe for money', () => {
expect(0.1 + 0.2).not.toBe(0.3);
});
En seis meses alguien intentará pasar un number por comodidad. Probablemente yo. El CI tiene que recordarnos por qué no.
El endpoint puede recibir "amountCents": 1050. Ahí validas: entero positivo, rango razonable, moneda si aplica.
Ahí conviertes a Money.fromCents(1050n) antes de ejecutar la lógica de la transferencia. Esa capa no debería ver importes sueltos como number. Esa fue otra lección del reto anterior: mezclar lo cómodo del HTTP con lo correcto en el negocio y pagar el precio meses después.
Curiosamente, no escribí el ledger ese día.
Escribí algo más pequeño. Pero cuando trabajas con dinero, los errores rara vez aparecen en la primera transferencia. Aparecen meses después. En una reconciliación. En una auditoría. En una investigación de soporte. En una diferencia de un centavo que nadie sabe explicar.
Por eso empecé aquí: definiendo qué significa el dinero en el código de negocio.
El ledger viene después. Primero necesitaba que el sistema supiera contar.
Serie: makingcode.dev/series/fintech-core-platform
Anterior: Por qué monolito modular
Código: github.com/AndresED/fintech-core-platform (libs/domain-common)