Project Objectives
This is the final capstone project. You will build a complete Mini Core Banking system, simultaneously applying all the principles we’ve covered:
- ✅ Part 1: Double-entry bookkeeping with an immutable Ledger
- ✅ Part 2: CIF, CASA, and Lending domain models
- ✅ Part 3: ACID transactions, Pessimistic Locking, and Idempotency
- ✅ Part 4: Event-driven design with the Outbox Pattern
- ✅ Part 5: Standardized message structures (ISO-inspired)
- ✅ Part 6: Audit trails and data classification
You can use any language: Go, Java, Python, Node.js, .NET — the architectural principles remain the same.
Step 1: Design the Database Schema
This is the foundation. Get this right, and everything else flows naturally.
-- ============================================================
-- 1. CUSTOMERS (CIF)
-- ============================================================
CREATE TABLE customers (
cif_number VARCHAR(20) PRIMARY KEY,
customer_type VARCHAR(15) NOT NULL CHECK (customer_type IN ('INDIVIDUAL','CORPORATE')),
full_name VARCHAR(255) NOT NULL,
id_number VARCHAR(30) UNIQUE NOT NULL,
kyc_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================
-- 2. ACCOUNTS (CASA)
-- ============================================================
CREATE TABLE accounts (
account_number VARCHAR(20) PRIMARY KEY,
cif_number VARCHAR(20) NOT NULL REFERENCES customers(cif_number),
account_type VARCHAR(30) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'VND',
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
current_balance BIGINT NOT NULL DEFAULT 0,
available_balance BIGINT NOT NULL DEFAULT 0,
version BIGINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================
-- 3. LEDGER ENTRIES (Double-Entry Bookkeeping)
-- ============================================================
CREATE TABLE ledger_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id UUID NOT NULL,
account_number VARCHAR(20) NOT NULL REFERENCES accounts(account_number),
entry_type CHAR(6) NOT NULL CHECK (entry_type IN ('DEBIT','CREDIT')),
amount BIGINT NOT NULL CHECK (amount > 0),
currency CHAR(3) NOT NULL,
balance_after BIGINT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Prevent edits to the ledger
CREATE RULE no_update_ledger AS ON UPDATE TO ledger_entries DO INSTEAD NOTHING;
CREATE RULE no_delete_ledger AS ON DELETE TO ledger_entries DO INSTEAD NOTHING;
-- ============================================================
-- 4. TRANSACTIONS (Idempotency Control)
-- ============================================================
CREATE TABLE financial_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key VARCHAR(64) UNIQUE NOT NULL,
type VARCHAR(30) NOT NULL, -- 'DEPOSIT','WITHDRAWAL','TRANSFER','FEE'
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING',
from_account VARCHAR(20),
to_account VARCHAR(20),
amount BIGINT NOT NULL,
currency CHAR(3) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- ============================================================
-- 5. OUTBOX (At-Least-Once Event Publishing)
-- ============================================================
CREATE TABLE outbox_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
topic VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ
);
-- ============================================================
-- 6. AUDIT LOG
-- ============================================================
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type VARCHAR(50) NOT NULL,
entity_id VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
actor_id VARCHAR(50) NOT NULL,
before_data JSONB,
after_data JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Step 2: Implement the Money Transfer Logic
This is the most critical use case. All steps below must happen within a single Database Transaction:
FLOW: POST /v1/transfers
Header: Idempotency-Key: <uuid-from-client>
STEP 1: Check Idempotency Key
→ If already completed: return old result (200 OK)
→ If processing: return 409 Conflict
→ If new: proceed
STEP 2: Validate Input
→ amount > 0
→ from_account != to_account
→ currency is valid
STEP 3: BEGIN DATABASE TRANSACTION
STEP 3a: Lock both accounts (order by ID to prevent deadlocks)
→ SELECT ... FOR UPDATE WHERE account_number IN (from, to) ORDER BY account_number
STEP 3b: Check business rules
→ available_balance(from) >= amount
→ status(from) = 'ACTIVE'
→ status(to) = 'ACTIVE'
STEP 3c: Update Balances
→ UPDATE accounts SET current_balance -= amount, available_balance -= amount WHERE from
→ UPDATE accounts SET current_balance += amount, available_balance += amount WHERE to
STEP 3d: Write Double-Entry Ledger (2 entries)
→ INSERT DEBIT entry (from_account, amount, balance_after_deduction)
→ INSERT CREDIT entry (to_account, amount, balance_after_addition)
STEP 3e: Insert financial_transaction record (status = COMPLETED)
STEP 3f: Insert idempotency_key record
STEP 3g: Insert outbox_events (topic: 'transfer.completed')
STEP 3h: Insert audit_log
STEP 4: COMMIT TRANSACTION
STEP 5: Return 202 Accepted + transaction_id
Step 3: Write the Invariant Check
Create an internal endpoint (or a cron job) to continuously verify ledger integrity:
-- Run this daily, trigger alerts if imbalance != 0
SELECT
currency,
SUM(CASE WHEN entry_type = 'DEBIT' THEN amount ELSE 0 END) AS total_debits,
SUM(CASE WHEN entry_type = 'CREDIT' THEN amount ELSE 0 END) AS total_credits,
SUM(CASE WHEN entry_type = 'DEBIT' THEN amount ELSE 0 END) -
SUM(CASE WHEN entry_type = 'CREDIT' THEN amount ELSE 0 END) AS imbalance
FROM ledger_entries
GROUP BY currency
HAVING SUM(CASE WHEN entry_type = 'DEBIT' THEN amount ELSE 0 END) !=
SUM(CASE WHEN entry_type = 'CREDIT' THEN amount ELSE 0 END);
Step 4: Stress Testing — The Final Exam
Once built, you MUST stress test the system by sending highly concurrent requests:
# Using k6 (JavaScript load testing tool)
# Scenario: 100 users transferring money simultaneously from Account A to B
k6 run --vus 100 --duration 30s transfer_test.js
# After testing, verify:
# 1. Did the total amount of money in the system change?
# 2. Is the ledger balanced? (DEBIT = CREDIT)
# 3. Were there any duplicate transactions?
# 4. Did any account balance drop below zero?
// transfer_test.js
import http from 'k6/http';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';
export default function () {
http.post('http://localhost:8080/v1/transfers', JSON.stringify({
from_account: 'ACC001',
to_account: 'ACC002',
amount: 1000,
currency: 'VND',
description: 'Stress test transfer'
}), {
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': uuidv4(), // Unique key per request
}
});
}
Project Completion Checklist
Core Banking Logic
- Double-entry ledger works correctly (DEBIT = CREDIT after every transaction)
- Ledger entries are immutable (no UPDATE/DELETE possible)
- Balance invariant check runs and returns 0
Concurrency & Safety
- Pessimistic locking prevents race conditions
- Lock ordering is consistent (prevents deadlocks)
- Idempotency key works (sending 10 identical keys → exactly 1 transaction)
- Accounts can never have a negative balance
Reliability
- Outbox pattern guarantees no events are lost
- API returns 202 Accepted quickly (non-blocking)
- Audit log meticulously records all actions
Stress Test
- 100 concurrent transfers → no money lost or magically created
- Ledger remains perfectly balanced
- No deadlock errors in application logs
What’s Next?
Once you have a functional Mini Core Banking system, you can extend it:
- Add a Lending module: Create loans, calculate daily interest, process repayments.
- Integrate ISO 8583: Build a simple Payment Switch to receive card messages and route them to the Core.
- CQRS Read Side: Add Elasticsearch for ultra-fast transaction history lookups.
- Rate Limiting & Fraud Detection: Detect anomalous transactions using a Rule Engine.
- Study Apache Fineract: Dive into the open-source code of a real-world Core Banking system.
Congratulations on completing the series! You now have a solid foundation to begin your journey as a Core Banking Developer. Remember: in this field, meticulousness and systems thinking are vastly more important than coding speed.