Case study, Fintech

A real-time ledger for Nordbank.

Nordbank ran on an overnight batch that posted the previous day's transactions each morning. We replaced it with a double-entry core that settles the moment money moves, at 12k transactions per second and a p99 of 40 ms, without a single batch window.

The engagement

From nightly batch to instant settlement.

Nordbank is a digital bank. Its ledger was correct but slow: balances were only trustworthy after the overnight run finished, so support, risk, and the mobile app all worked from stale numbers for most of the banking day. We rebuilt the ledger as an event-sourced, double-entry service that posts and settles each transaction inline, holds its latency budget under peak load, and produces an audit trail the bank can reconcile at any point in time.

Digital banking Double-entry Event sourcing Exactly-once

Engagement at a glance

ClientNordbank
SectorDigital banking
Engagement9 months
Team4 engineers
StackGo, PostgreSQL, Kafka, gRPC
In production, managed by ECORTIQ

The challenge

A ledger that could not keep up with the bank.

The old system settled once a day. That was tolerable when Nordbank was small. It stopped being tolerable when transaction volume grew past what a single nightly window could absorb, and when product teams needed live balances to build against. The rebuild had one non-negotiable constraint: a ledger is allowed to be slow or wrong, never both, and this one could no longer be slow.

We scoped three failure modes that mattered and designed the system around them: money must never be double-counted, a crash must never lose or replay a posting, and the day's total debits must always equal its total credits, provably, at any timestamp.

  • Overnight batch settlementBalances lagged reality by up to a full banking day. Support and risk teams worked from numbers they could not fully trust until the morning run completed.
  • Growth pressureVolume was on track to outgrow the batch window. A run that overran into business hours would have delayed every customer balance behind it.
  • Strict correctnessA bank ledger is the system of record. Every posting has to be exact, idempotent, and reconcilable against the general ledger to the cent.
  • AuditabilityRegulators and internal audit needed a complete, ordered history: what changed, when, and why, reconstructable long after the fact.

The approach

Four decisions that made settlement safe at speed.

We did not chase novelty. The design leans on well understood patterns, applied carefully and instrumented so the bank can prove each guarantee holds in production.

01

A double-entry core

Every movement is two balanced postings against accounts. The invariant that debits equal credits is enforced inside the write path, not checked after the fact, so an unbalanced transaction can never be committed in the first place.

02

Event sourcing with an outbox

The ledger's state is derived from an append-only log of posting events. State changes and their outbound events are written in the same PostgreSQL transaction through an outbox table, so what the rest of the bank sees can never drift from what was actually committed.

03

Idempotent processing

Every command carries a client-supplied idempotency key. A retry, a redelivery, or a duplicate from an upstream service resolves to the exact same posting, so at-least-once transport becomes safe to build on without ever double-charging an account.

04

Exactly-once settlement

The outbox relay and a Kafka consumer with committed offsets give effectively-once delivery downstream. Combined with idempotent writes, each transaction settles exactly one time even across broker restarts, consumer rebalances, and partial failures.

The architecture

One write path, two durable stores, no batch.

A gRPC API accepts a transfer, resolves its idempotency key, and writes the balanced postings and their event to PostgreSQL in a single transaction. The outbox relay streams committed events to Kafka, where downstream consumers update the read model, feed risk, and notify the mobile app. Settlement happens on the write, so there is nothing left to run overnight.

Read traffic is served from a projection kept current off the same event stream, which keeps the hot settlement path free of query load. The log below is a trimmed trace of a single transfer moving through that path.

gRPC ingress PostgreSQL Outbox relay Kafka
settle.trace
# transfer accepted at gRPC ingress
> Ledger.Post(idem="txn-8f3a1c")
> idempotency miss, new command accepted
# two balanced postings, one tx
> debit acct 4471 -120.00 EUR
> credit acct 9930 +120.00 EUR
> invariant debits==credits ok
> commit pg + outbox event durable
# relay to stream, project read model
> outbox → kafka partition 6
> settled in 31ms, p99 budget held

Results

The batch is gone, and the numbers held.

The ledger has run the settlement path in production for the full engagement and beyond. Figures below are steady-state production measurements.

12k tx/s
Sustained settlement throughput at peak
40 ms
p99 latency from posting to settled
99.99%
Uptime for the ledger service over twelve months
Removed
The overnight batch settlement window, entirely

Live balances

Support, risk, and the mobile app now read balances that are correct to the second, not to the previous business day.

Reconciles to the cent

The event log lets the bank reconstruct any account's state at any timestamp, which cut month-end reconciliation from hours to minutes.

Room to grow

Throughput headroom above current peak means the next tier of volume needs more partitions, not another architecture.

ECORTIQ took a ledger we were afraid to touch and turned it into the most reliable service we run. They understood the failure modes better than we did, and they proved every guarantee instead of asserting it.
JK
Jonas Keller
VP Engineering, Nordbank

Start

Have a ledger you are afraid to touch?

Tell us where the correctness or the latency hurts. We will tell you honestly whether we are the right team to rebuild it.