Building Fintech Systems
That Stay Fast and Stay Compliant
Code BEAM Berlin 2025
Fast vs Compliant?
People will tell you to choose one.
These people are wrong.
Why This Room
In 30 minutes you'll leave with:
- a ledger-first layout you can map to your system
- a money type you can implement on Monday
- a trace and audit field list that makes audits as easy as queries
Who Am I
- Klarna's first CTO
- Author of The BEAM Book
- Programming since 1980
I have made every mistake in this talk at least twice.
The Fintech Scaling Problem
PSD2 · GDPR · AML · PCI · SOC2
Rules pile up.
Each jurisdiction adds more.
Stakes: money, licenses, trust.
Also: your sanity.
Two Bad Paths
Start
|
---------------
| |
Speed-first Compliance-first
🚀 ✅
↓ ↓
Compliance Slow releases
fire drills Lost ground
The Goal
Architecture + Culture
→ Speed and Compliance
Ledger-Centric Design
Typed Money
❌ "149.99" (string)
❌ 149.99 (float)
✅ {amount: 14999, currency: "SEK", scale: 2}
Why the BEAM Helps
- Scaling out of the box
- Bit syntax for protocols
- Arbitrary-precision integers
- Immutable data
- Isolated processes
- Supervision trees
Pattern: Isolation Boundaries
Pattern: Safe Change at Speed
- Adapter per PSP, contract tests
- Queues at edges, idempotent handlers
- Feature switches per integration
- Hot code upgrades when needed
Traceability and Simplicity
- Logs + metrics + traces = one story
- Single
trace_id
across transaction
- Example: GDPR with UUIDs + mapping table
Audit Trail as Query
{
"trace_id": "7b3bf470...",
"actor": "user:U123",
"timestamp": "2025-09-30T14:35:12Z",
"event": "payment.authorized",
"amount_before": 0,
"amount_after": 14999,
"psp": "AcmePay",
"region": "SE"
}
Culture: YBYOYR
You Build it · You Own it · You Run it
Failure vs Success
❌ Failure |
✅ Success |
Floats in money |
Typed money |
No trace IDs |
Trace IDs everywhere |
PSP outages cascade |
Isolation boundaries |
Months of retrofits |
Clean audits, fast launches |
Takeaway
Friday deploys.
Monday audits.
One system.
Both work. Or neither does.
Live Demo (Optional): Erlang Mini-Ledger
- Append-only ledger with a hash chain + Merkle root over balances.
- Show: immutable history, crash safety, proof-of-state.
Observer
observer:start().
The Code: ledger.erl (Core)
post(Account, Delta, Currency) ->
Ts = erlang:system_time(millisecond),
Entry = #{ts => Ts, acct => Account,
delta => Delta, ccy => Currency},
Prev = prev_hash(),
TxHash = sha256(term_to_binary(Entry)),
BlockHash = sha256(<<Prev/binary, TxHash/binary>>),
ets:insert(?CHAIN, {Ts, Entry, TxHash, Prev, BlockHash}),
update_balance(Account, Delta, Currency),
ets:insert(?META, {?PREV, BlockHash}),
{ok, BlockHash}.
The Code: Merkle Root
state_root() ->
Leafs = lists:map(
fun({A,B,C}) ->
sha256(<<(term_to_binary(A))/binary,
(term_to_binary(B))/binary,
C/binary>>)
end,
lists:sort(ets:tab2list(?BAL))),
merkle:root(Leafs).
Compact commitment to all balances. Any change produces a different root.
Step 1 — Start the Ledger
1> c(ledger_store), c(ledger), c(merkle), ledger:start().
ok
- ETS-backed append-only chain
- Hash = SHA256(prev_hash || txn)
- Balances updated from entries
ledger_store
owns the ETS tables (heir pattern)
Step 2 — Post Transactions
2> ledger:post(user_1, 14999, <<"SEK">>).
{ok,<<...>>}
3> ledger:post(user_1, -2500, <<"SEK">>).
{ok,<<...>>}
4> ledger:balance(user_1).
12499
- Entries are appended; nothing is overwritten.
Step 3 — Prove State (Merkle Root)
5> ledger:state_root().
<<RootHash:256/bits>>
6> ledger:balances().
[{user_1,12499,<<"SEK">>}]
state_root()
is the Merkle root over the sorted balance set.
- Any stakeholder can recompute and match.
Step 4 — Fault Containment (Heir Pattern)
7> whereis(ledger_worker).
<0.123.0>
8> exit(whereis(ledger_worker), kill).
true
9> ledger:balance(user_1).
12499 % Still works! Tables transferred to heir
10> ledger:start_worker().
{ok, <0.125.0>}
11> ledger:balance(user_1).
12499 % Data intact after worker restart
- Worker dies → tables transfer to
ledger_store
(heir).
What Are Merkle Proofs?
Problem: Your auditor needs to verify one account balance without accessing your entire database.
Solution: Merkle tree, a cryptographic commitment to all balances.
- Each balance becomes a leaf hash
- Pairs combine into parent hashes
- Final root commits to entire state
Merkle Tree Structure
Root (6A93...)
/ \
H_AB H_CD
/ \ / \
H_A H_B H_C H_D
| | | |
user_1 user_2 user_3 user_4
12499 4000 8500 2100
To prove user_2 balance:
- Show leaf H_B (hash of user_2, 4000, SEK)
- Show path: [H_A (left), H_CD (right)]
- Anyone can recompute: hash(hash(H_A, H_B), H_CD) = Root
Why This Matters for Fintech
- Privacy: Prove one balance without revealing others
- Compact: Log N proof size (8 hashes for 256 accounts)
- Verifiable: Anyone can check against published root
- Historical: Generate proofs for any past state
This is what makes blockchains auditable.
You don't need a blockchain: plain Merkle trees in Erlang work fine.
How Verification Works
You publish: Root hash 6A93...
User claims: "I have 4000 SEK"
User provides proof:
- Leaf: hash(user_2, 4000, SEK) = 8C75...
- Path: [H_A (left sibling), H_CD (right sibling)]
Verifier computes:
- Step 1: hash(H_A, 8C75...) = H_AB
- Step 2: hash(H_AB, H_CD) = 6A93...
- Check: Does this match published root? Yes → Valid
Live Demo: Merkle Proofs
12> c(proof_pp).
{ok, proof_pp}
13> {ok, H1} = ledger:post(user_2, 5000, <<"SEK">>).
{ok, <<H1:256/bits>>}
14> {ok, H2} = ledger:post(user_2, -1000, <<"SEK">>).
{ok, <<H2:256/bits>>}
15> {ok, P} = ledger:proof(user_2).
{ok, #{balance => 4000, ...}}
Understanding the Proof Structure
16> proof_pp:show(P).
=== Proof of Balance ===
Account : user_2
Balance : 4000 "SEK"
Leaf (account hash):
8C75A23F...4E9FAE12
Path (2 siblings):
-> left 91AB4C2D...8F331D0A
-> right 0F88E7B1...3A7CC491
Root :
6A9312B7...9C4F8E2D
Verify the Proof
17> merkle_proof:verify(user_2, 4000, <<"SEK">>, maps:get(proof, P)).
true
Historical Proof (Rewind to H1)
18> {ok, P1} = ledger:proof_at(user_2, H1).
{ok, #{balance => 5000, at_block => H1, ...}}
19> proof_pp:show(P1).
=== Proof of Balance ===
Account : user_2
Balance : 5000 "SEK"
At block: 3A7F8E12...9C2B4D76
Leaf (account hash):
2F4C9A81...6D7E3B92
Path (1 siblings):
-> right 8B3E2C1F...4A9D7F05
Root :
D4F8A2E6...7B1C9E34
20> merkle_proof:verify(user_2, 5000, <<"SEK">>, maps:get(proof, P1)).
true
From Reliability to Verifiable State
You probably already know:
- Don't use floats for money
- Use supervisors for isolation and recovery
Now go further:
- Ledgers: every change append-only, reconstruct able
- CRDTs: concurrent updates that converge without locks
- Patricia-Merkle Trees: cryptographic proof of balances and state
Next Steps
