Loading home…
Loading home…
Loading article…
What happens when money stops being a number and becomes a responsibility? I asked myself that looking at amountCents in my POST /transfer draft. That day I wrote Money.

No spam — just a note when I publish something new on backend, cloud, and architecture.
One email when a post goes live. Unsubscribe anytime.

A LinkedIn post on fintech architecture, a recent technical challenge, and a decision to rebuild a banking core from scratch — slower this time, to actually learn.

Three seconds on the spinner. Timeout on screen. Did the money move? I reopened my Arkano challenge next to a fintech architecture map — and understood why POST /transfer was never just an endpoint.

Six boxes on the diagram. Six microservice temptations. I almost fell for it again — until I got that the full map doesn't mean shipping it all in week one, and splitting repos doesn't split the domain
Last week, closing the modular monolith piece, I left the building with one department occupied.
fintech-core-platform had Phase 0 in the repo: one deploy, accounts you could already create, business logic separated from the framework and from HTTP, tests that ran without starting PostgreSQL. When I closed I said transfers and ledger entries would come soon, without multiplying services yet. But first, something less glamorous: defining what money means in code. Cents. bigint. No float.
I thought that once I said that out loud, it was time to move money. I opened the transfer endpoint draft.
POST /transfer
And I got stuck on a single property:
{ "amountCents": 1050 }
Nothing extraordinary. Just an amount.
But that property made me stop.
I remembered something I'd written months ago in a technical challenge. A project that went well, passed tests, met requirements, and ended up on GitHub. Account balance was simply this:
from.balance -= dto.amount;
to.balance += dto.amount;
For a long time I didn't see a problem. It worked.
Until I asked myself a simple question: what happens when money stops being a number and becomes a responsibility?
That's when I understood the problem wasn't the ledger. Not idempotency. Not timeouts. Not even consistency across services.
The problem was I still hadn't defined what money meant inside the system.
A user sees $10.50. The system shouldn't see a decimal. It should see 1050 cents. Exactly 1050. Not one more. Not one less.
When I started building this fintech core from zero, I decided that would be the first concept I'd model properly. Even before transfers.
I looked at the options that always show up in technical posts and that I'd skipped in a hurry:
| Approach | Why it tempted me | Why I dropped it |
|---|---|---|
float / number |
Fast, native in JS | 0.1 + 0.2 is not 0.3 |
DB decimal + string |
Sounds "accounting-grade" | Parsing and TypeScript friction |
Cents + bigint |
Less comfortable at first | Exact integers, rules in business logic |
I didn't pick bigint because I'll move billions tomorrow. I picked it because I want adding and subtracting money in this project to be integer arithmetic, not a gamble.
PostgreSQL decimal comes with the ledger. This type knows nothing about the database or JSON.
Curiously, I didn't write the ledger that day.
I ended up building something much smaller: a class called Money.
No endpoints. No database. No events. No infrastructure.
But it's probably one of the most important pieces in the whole project.
It lives in @fintech/domain-common, the monorepo shared lib, because Accounts, Transfers, and the ledger will speak the same language:
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 and subtract return a new Money. They don't mutate. Mix currencies and the code fails with a clear error instead of silently adding the wrong thing.
Tests run without Docker. I left one that looks obvious and isn't:
it('should document that JS float arithmetic is unsafe for money', () => {
expect(0.1 + 0.2).not.toBe(0.3);
});
In six months someone will try passing a number for convenience. Probably me. CI has to remind us why not.
The endpoint can receive "amountCents": 1050. That's where you validate: positive integer, sane range, currency if needed.
You convert to Money.fromCents(1050n) before running the transfer logic. That layer shouldn't see bare number amounts. Another lesson from the earlier challenge: mix what's convenient in HTTP with what's correct in the business layer and pay for it months later.
Curiously, I didn't write the ledger that day.
I wrote something smaller. But when you work with money, errors rarely show up on the first transfer. They show up months later. In reconciliation. In an audit. In a support investigation. In a one-cent difference no one can explain.
That's why I started here: defining what money means in the business logic.
The ledger comes later. First I needed the system to know how to count.
Series: makingcode.dev/series/fintech-core-platform
Previous: Why modular monolith
Code: github.com/AndresED/fintech-core-platform (libs/domain-common)