Building Fintech Systems
That Stay Fast and Stay Compliant
Message example
Msg = #{
trace_id => T,
actor => <<"3f7e8c4-9d2b-4a15-8e6f-1c5b3d4a7f9e">>,
op => <<"authorize">>,
amount => 14999, ccy => <<"SEK">>, scale => 2,
meta => #{psp => <<"AcmePay">>,
idempotency_key => T }
}.
Ledger-side dedup idea (conceptual)
Maintain a dedup ETS table: dedup: seen(T) -> true | false.
On post:
if seen(T) -> {ok, already_applied, Prev_result};
true -> append(E),
mark_seen(T),
{ok, new, Result}
end
- 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
- Ledgers: every change append-only, reconstructable
- CRDTs: concurrent updates that converge without locks
- Patricia-Merkle Trees: cryptographic proof of balances and state
Worker
Handles one task. Dies after. You can pool them if needed.
Resource owner
Keeps state. A cart, a session, a WebSocket.
Router
Receives, decides, forwards. Never stores.
Gatekeeper
Limits something. One message at a time, access to a resource.
Observer
Watches processes, reacts to conditions.
Worker: Short-lived workers
Spawn, do one job, exit. Use these for things like sending emails, updating logs, writing audit trails.
Worker: Pooled workers
Lives in a pool when not running, does one job, returns to pool. Can keep some state, like configuration.
Router: Broadcaster
Fan out of messages.
Router: Director
Route messages to the right receiver.
Gatekeeper: Flow controllers
Processes that gate, synchronize, or sequence other flows.
They manage contention, enforce ordering, or regulate access.
Gatekeeper: Rate limiters
Provides back pressure.
Gatekeeper: Circuit breakers
When subsystems stall or crash, they keep the damage local.
Resource owner: Entity owner
Processes that wrap state.
A payment intent. A socket. A user.
Can be implemented with a gen_server.
Resource owner: Aggregator
Collector, windowing, rollups.
Observer: Sentinel
Watchdogs, deadlines, cancellations.
Observer: Supervisor
Watches others. Restarts them. Never does real work.
They know who to restart and when to walk away.