📖 Bản tiếng Việt (Vietnamese Edition)
Prerequisite: Read Part 1: Double-Entry Bookkeeping for ledger schema and balance invariant fundamentals.
Core Banking Domain Modeling: CIF, CASA & Lending Guide
Answer-first: Core banking domain architecture revolves around three fundamental bounded contexts: Customer Information File (CIF) for identity management and KYC compliance, Current & Savings Accounts (CASA) for high-velocity transactional deposit ledgers, and Lending for multi-period credit amortization. Decoupling these domains into autonomous Go microservices communicating via gRPC contracts eliminates database lock contention between daytime retail transactions and nightly End-of-Day (EOD) interest accrual batch jobs.
1. Domain Decomposition: CIF, CASA, and Lending
The three pillars of commercial retail banking operate with distinct transactional velocity and data retention models:
flowchart TD
subgraph CIF_Domain ["1. Customer Information File (CIF)"]
Party["Party Entity (Individual / Corporate)"]
KYC["KYC Verification & AML Risk Tier"]
Limits["Daily Transaction & Withdrawal Limits"]
end
subgraph CASA_Domain ["2. Deposit & CASA Service (High Velocity)"]
CurrentAcc["Current Accounts (Chequing / Overdraft)"]
SavingsAcc["Savings Accounts (Daily Interest Accrual)"]
HoldEngine["Funds Reservation & Active Holds"]
end
subgraph Lending_Domain ["3. Lending & Credit Service (Analytical)"]
LoanOrigination["Credit Assessment & Underwriting"]
Amortization["Amortization Schedule Engine"]
Collection["Delinquency Aging & Provisioning (IFRS 9)"]
end
CIF_Domain -->|"Entity Binding & Limits"| CASA_Domain
CIF_Domain -->|"Credit Score & CIF ID"| Lending_Domain
Lending_Domain -->|"Disbursement & Auto-Debit Repayments"| CASA_Domain
2. Lending Account Lifecycle State Machine
A loan contract transitions through a rigorous state machine enforcing strict regulatory and accounting milestones:
stateDiagram-v2
[*] --> DRAFT: Customer Applies
DRAFT --> UNDERWRITING: Submit Application
UNDERWRITING --> REJECTED: Risk Score Failed
UNDERWRITING --> APPROVED: Credit Approved
APPROVED --> DISBURSED: Drawdown to CASA Account
state DISBURSED {
[*] --> ACTIVE
ACTIVE --> DELINQUENT: Missed Due Date (> 10 DPD)
DELINQUENT --> ACTIVE: Overdue Payment Received
DELINQUENT --> DEFAULTED: DPD > 90 (NPL Group 3+)
}
ACTIVE --> FULLY_PAID: Final Installment Settled
DEFAULTED --> WRITTEN_OFF: Charged-off to Off-Balance Sheet
FULLY_PAID --> [*]
REJECTED --> [*]
WRITTEN_OFF --> [*]
3. CASA Daily Interest Accrual Mechanics
Interest calculation on deposit accounts is performed nightly during the End-of-Day (EOD) batch process. Rather than calculating interest on monthly balances, banking regulations require daily accruals based on end-of-day ledger balances:
$$\text{Daily Accrual} = \frac{\text{Ledger Balance} \times \text{Annual Interest Rate}}{365}$$
-- Daily Interest Accrual Table Schema
CREATE TABLE daily_interest_accruals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES accounts(id),
accrual_date DATE NOT NULL,
closing_balance BIGINT NOT NULL,
annual_rate_bps INT NOT NULL, -- Stored in Basis Points (1 bps = 0.01%)
accrued_amount BIGINT NOT NULL, -- Minor currency unit
is_capitalized BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
UNIQUE (account_id, accrual_date)
);
4. Amortization Algorithm Implementation in Go
Lending engines support two primary repayment formulas: Equal Installment (Annuity) and Equal Principal (Reducing Balance). Below is the production Go implementation:
package lending
import (
"math"
"time"
)
type RepaymentScheduleItem struct {
Period int
DueDate time.Time
Installment int64 // Minor currency unit
PrincipalAmount int64
InterestAmount int64
RemainingBalance int64
}
// CalculateEqualPrincipal calculates reducing balance loan amortization.
func CalculateEqualPrincipal(principal int64, annualRate float64, tenureMonths int, startDate time.Time) []RepaymentScheduleItem {
schedule := make([]RepaymentScheduleItem, tenureMonths)
monthlyPrincipal := principal / int64(tenureMonths)
monthlyRate := annualRate / 12.0
currentBalance := principal
for i := 1; i <= tenureMonths; i++ {
// Calculate interest on remaining balance using Banker's Rounding
interest := int64(math.Round(float64(currentBalance) * monthlyRate))
// Adjust final period to eliminate rounding drift
pAmount := monthlyPrincipal
if i == tenureMonths {
pAmount = currentBalance
}
currentBalance -= pAmount
dueDate := startDate.AddDate(0, i, 0)
schedule[i-1] = RepaymentScheduleItem{
Period: i,
DueDate: dueDate,
Installment: pAmount + interest,
PrincipalAmount: pAmount,
InterestAmount: interest,
RemainingBalance: currentBalance,
}
}
return schedule
}
Frequently Asked Questions
How does CIF prevent duplicate customer records across disparate banking channels?
How is daily interest accrued on millions of savings accounts without degrading database performance?
accounts table. Instead, an asynchronous EOD worker takes a consistent snapshot of closing balances as of the midnight cutoff time. Calculations execute in parallel Go worker pools (partitioned by account hash), writing accrual records into an append-only daily_interest_accruals table. At month-end, a single aggregate transaction capitalizes the interest into the customer’s live balance.