Designing scalable fintech systems
Clear money boundaries, idempotent commands, and ledgers your finance team can reason about — not just a bigger Postgres cluster.
Money systems fail expensively: duplicate charges, settlements stuck in ambiguous states, reconciliation spreadsheets that diverge from the database of record. The fix is rarely a bigger database — it is explicit boundaries between initiation, authorization, clearing, and reconciliation, each with its own invariants and audit trail.
Idempotent payment intent
Retries are inevitable. The client must supply a stable idempotency key; the server must treat it as a unique business key, not a nice-to-have header. The sketch below shows the happy path: return the same intent if the key was already processed.
import { randomUUID } from "node:crypto";
type CreatePayment = {
amount: number;
currency: string;
/** Client-generated; retries MUST reuse the same key */
idempotencyKey: string;
};
export async function createPaymentIntent(cmd: CreatePayment) {
const existing = await store.findByIdempotencyKey(cmd.idempotencyKey);
if (existing) return existing;
const intent = {
id: randomUUID(),
...cmd,
status: "requires_confirmation" as const,
};
await store.save(intent);
return intent;
}Ledger and reconciliation
If finance cannot explain your ledger model in one whiteboard session, operations will pay the tax forever. Model balances as append-only events where possible; make “why is this amount here?” answerable from data, not tribal knowledge. Pair that with reconciliation jobs that match PSP statements to internal events — and alert when drift exceeds a threshold, not when someone notices a spreadsheet.
Throughput and debuggability go together: high QPS means nothing if every incident turns into a multi-day forensic. Invest in observability on money paths as early as you invest in caching.
