Mục tiêu của Dự án

Đây là bài tập cuối chuỗi. Bạn sẽ tự tay xây dựng một Mini Core Banking hoàn chỉnh, áp dụng đồng thời mọi nguyên lý đã học:

  • Phần 1: Kế toán kép với bảng Ledger immutable
  • Phần 2: CIF, CASA, Lending domain models
  • Phần 3: ACID transactions, Pessimistic Locking, Idempotency
  • Phần 4: Event-driven với Outbox Pattern, kiến trúc Service
  • Phần 5: Cấu trúc thông điệp chuẩn (ISO-inspired)
  • Phần 6: Audit trail, data classification

Bạn có thể dùng bất kỳ ngôn ngữ nào: Go, Java, Python, Node.js, .NET — nguyên lý không thay đổi.


Bước 1: Thiết kế Database Schema

Đây là nền tảng. Làm đúng bước này, mọi thứ còn lại sẽ tự nhiên hơn.

-- ============================================================
-- 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()
);

-- Ngăn sửa/xóa 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 (Event Publishing đảm bảo At-Least-Once)
-- ============================================================
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()
);

Bước 2: Implement Logic Chuyển Tiền (Trái tim của hệ thống)

Đây là use case quan trọng nhất. Phải đảm bảo tất cả bước dưới đây trong một Database Transaction duy nhất:

FLOW: POST /v1/transfers
  Header: Idempotency-Key: <uuid-from-client>

STEP 1: Kiểm tra Idempotency Key
  → Nếu đã xử lý rồi: return kết quả cũ (200 OK)
  → Nếu đang xử lý: return 409 Conflict
  → Nếu chưa có: tiếp tục

STEP 2: Validate input
  → amount > 0
  → from_account != to_account
  → currency hợp lệ

STEP 3: BEGIN DATABASE TRANSACTION
  
  STEP 3a: Lock hai tài khoản (theo thứ tự ID để tránh deadlock)
    → SELECT ... FOR UPDATE WHERE account_number IN (from, to) ORDER BY account_number

  STEP 3b: Kiểm tra điều kiện nghiệp vụ
    → available_balance(from) >= amount
    → status(from) = 'ACTIVE'
    → status(to) = 'ACTIVE'

  STEP 3c: Cập nhật số dư
    → UPDATE accounts SET current_balance -= amount, available_balance -= amount WHERE from
    → UPDATE accounts SET current_balance += amount, available_balance += amount WHERE to

  STEP 3d: Ghi Double-Entry Ledger (2 bút toán)
    → INSERT DEBIT entry (from_account, amount, balance_after_deduction)
    → INSERT CREDIT entry (to_account, amount, balance_after_addition)

  STEP 3e: Ghi financial_transaction record (status = COMPLETED)

  STEP 3f: Ghi idempotency_key record

  STEP 3g: Ghi outbox_events (topic: 'transfer.completed')

  STEP 3h: Ghi audit_log

STEP 4: COMMIT TRANSACTION

STEP 5: Return 202 Accepted + transaction_id

Bước 3: Viết Invariant Check

Thêm một endpoint nội bộ (hoặc cron job) để liên tục kiểm tra tính toàn vẹn của sổ cái:

-- Chạy cái này hàng ngày, alert nếu 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);

Bước 4: Stress Test — Bài kiểm tra cuối cùng

Sau khi xây dựng xong, bạn PHẢI stress test bằng cách gửi nhiều request concurrent:

# Dùng k6 (JavaScript load testing tool)
# Kịch bản: 100 user cùng chuyển tiền đồng thời từ tài khoản A sang B

k6 run --vus 100 --duration 30s transfer_test.js

# Sau khi test xong, kiểm tra:
# 1. Tổng tiền trong hệ thống có bị thay đổi không?
# 2. Sổ cái có cân bằng không? (DEBIT = CREDIT)
# 3. Có giao dịch nào bị trùng lặp không?
# 4. Có tài khoản nào bị âm không?
// 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,   // 1.000 đồng
        currency:     'VND',
        description:  'Stress test transfer'
    }), {
        headers: {
            'Content-Type': 'application/json',
            'Idempotency-Key': uuidv4(),  // Mỗi request có key riêng
        }
    });
}

Checklist hoàn thành dự án

Core Banking Logic

  • Double-entry ledger hoạt động đúng (DEBIT = CREDIT sau mọi giao dịch)
  • Ledger entries là immutable (không thể UPDATE/DELETE)
  • Balance invariant check chạy được và trả về 0

Concurrency & Safety

  • Pessimistic locking ngăn race condition
  • Lock order nhất quán (tránh deadlock)
  • Idempotency key hoạt động (gửi 10 lần cùng key → chỉ 1 giao dịch)
  • Tài khoản không bao giờ bị âm

Reliability

  • Outbox pattern đảm bảo event không bị mất
  • API trả về 202 Accepted (không block chờ hoàn tất)
  • Audit log ghi đầy đủ mọi thao tác

Stress Test

  • 100 concurrent transfers → không có tiền bị mất/tạo ra
  • Ledger cân bằng sau stress test
  • Không có deadlock error trong logs

Bước tiếp theo sau khi hoàn thành

Khi bạn đã build được một Mini Core Banking hoạt động đúng, bạn có thể tiếp tục mở rộng:

  1. Thêm Lending module: Tạo khoản vay, tính lãi tự động, thu nợ định kỳ.
  2. Tích hợp ISO 8583: Xây dựng một Payment Switch đơn giản nhận thông điệp thẻ và gọi vào Core.
  3. CQRS Read Side: Thêm Elasticsearch để tìm kiếm lịch sử giao dịch cực nhanh.
  4. Rate Limiting & Fraud Detection: Phát hiện giao dịch bất thường theo Rule Engine.
  5. Nghiên cứu Apache Fineract: Source code mã nguồn mở của một Core Banking thực tế.

Tài nguyên tham khảo mở rộng

Tài nguyênLoạiNội dung
Apache FineractOpen SourceCore Banking đầy đủ cho tài chính vi mô
jPOS ProjectFramework + SáchISO 8583, payment switch
Ledger.io BlogBlog kỹ thuậtDouble-entry accounting code
Thought Machine BlogBlog kiến trúcVault Core — Core Banking hiện đại
Mambu Dev PortalDocsSaaS Core Banking API
BIAN.orgChuẩn kiến trúcBanking Industry Architecture Network
Designing Data-Intensive ApplicationsSách (Martin Kleppmann)Nền tảng hệ thống phân tán

Chúc mừng bạn đã hoàn thành chuỗi bài học! Bạn đã có đầy đủ nền tảng để bắt đầu hành trình trở thành một Core Banking Developer. Hãy nhớ: trong lĩnh vực này, sự cẩn thận và tư duy hệ thống còn quan trọng hơn tốc độ code.