Functional Specifications
- Double-Entry Ledger Integrity: Force every transaction to contain balanced matching components (Debits + Credits), maintaining mathematical alignment.
- Integer Representation (Cents): Avoid standard float/double precision errors by keeping monetary metrics as integer cents.
- Immutability Guarantee: Ledger records are append-only. Corrections require appending inverse double-entry reversal pairs.
- Strict Idempotency: Enforce a unique reference identifier to ensure each transaction produces exactly one journal entry pair.
Clean reference class setups featuring normal-balance equations, atomic double-entry logs, and transaction reversals:
// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.math.BigDecimal;
import java.time.Instant;
enum AccountType {
ASSET, // Normal Debit (Debit increases, Credit decreases)
LIABILITY, // Normal Credit (Credit increases, Debit decreases)
EQUITY, // Normal Credit
INCOME, // Normal Credit
EXPENSE // Normal Debit
}
class Account {
private final String accountId;
private final String name;
private final AccountType type;
private final String currency;
public Account(String accountId, String name, AccountType type, String currency) {
this.accountId = accountId;
this.name = name;
this.type = type;
this.currency = currency;
}
public String getAccountId() { return accountId; }
public String getName() { return name; }
public AccountType getType() { return type; }
public String getCurrency() { return currency; }
}
class JournalEntry {
private final String entryId;
private final String referenceId; // External Transaction / Idempotency Key
private final String debitAccountId;
private final String creditAccountId;
private final long amountCents; // Cents used to avoid IEEE 754 float precision errors
private final Instant timestamp;
private final String description;
public JournalEntry(String entryId, String referenceId, String debitAccountId, String creditAccountId, long amountCents, String description) {
this.entryId = entryId;
this.referenceId = referenceId;
this.debitAccountId = debitAccountId;
this.creditAccountId = creditAccountId;
this.amountCents = amountCents;
this.timestamp = Instant.now();
this.description = description;
}
public String getEntryId() { return entryId; }
public String getReferenceId() { return referenceId; }
public String getDebitAccountId() { return debitAccountId; }
public String getCreditAccountId() { return creditAccountId; }
public long getAmountCents() { return amountCents; }
public Instant getTimestamp() { return timestamp; }
public String getDescription() { return description; }
}
class BankingLedgerSystem {
// In-memory Ledger Data Stores
private final Map<String, Account> accounts = new HashMap<>();
private final List<JournalEntry> journal = new ArrayList<>();
private final Set<String> processedReferenceIds = new HashSet<>(); // Idempotency check
public synchronized void createAccount(Account account) {
if (accounts.containsKey(account.getAccountId())) {
throw new IllegalArgumentException("Account already exists");
}
accounts.put(account.getAccountId(), account);
}
// Atomic double-entry poster
public synchronized JournalEntry postTransaction(String referenceId, String debitAccId, String creditAccId, long amountCents, String description) {
if (amountCents <= 0) {
throw new IllegalArgumentException("Transaction amount must be positive");
}
// 1. Idempotency Check
if (processedReferenceIds.contains(referenceId)) {
throw new IllegalStateException("Transaction already processed: " + referenceId);
}
// 2. Validate Accounts exist and have matching currencies
Account debitAcc = accounts.get(debitAccId);
Account creditAcc = accounts.get(creditAccId);
if (debitAcc == null || creditAcc == null) {
throw new IllegalArgumentException("Debit or Credit account does not exist");
}
if (!debitAcc.getCurrency().equals(creditAcc.getCurrency())) {
throw new IllegalArgumentException("Currency mismatch across double-entry pair");
}
// 3. Post to Journal atomically
String entryId = UUID.randomUUID().toString();
JournalEntry entry = new JournalEntry(entryId, referenceId, debitAccId, creditAccId, amountCents, description);
journal.add(entry);
processedReferenceIds.add(referenceId);
return entry;
}
// Derive balance using appropriate normal balance types
public synchronized long getBalance(String accountId, Instant asOf) {
Account account = accounts.get(accountId);
if (account == null) throw new IllegalArgumentException("Account not found");
long debits = 0;
long credits = 0;
for (JournalEntry entry : journal) {
if (entry.getTimestamp().isAfter(asOf)) continue;
if (entry.getDebitAccountId().equals(accountId)) {
debits += entry.getAmountCents();
}
if (entry.getCreditAccountId().equals(accountId)) {
credits += entry.getAmountCents();
}
}
// Apply accountant equation based on normal balances
if (account.getType() == AccountType.ASSET || account.getType() == AccountType.EXPENSE) {
return debits - credits; // Normal Debit
} else {
return credits - debits; // Normal Credit
}
}
// Post reversal to correct a transaction without modifying history
public synchronized JournalEntry postReversal(String originalReferenceId, String newReferenceId) {
JournalEntry target = null;
for (JournalEntry entry : journal) {
if (entry.getReferenceId().equals(originalReferenceId)) {
target = entry;
break;
}
}
if (target == null) throw new IllegalArgumentException("Original transaction not found");
// Reverse accounts: original Debit becomes Credit, original Credit becomes Debit
return postTransaction(
newReferenceId,
target.getCreditAccountId(), // Swap Credit to Debit
target.getDebitAccountId(), // Swap Debit to Credit
target.getAmountCents(),
"Reversal adjustment for transaction " + originalReferenceId
);
}
// Trial balance check: Sum(debits) must EQUAL Sum(credits) at all times
public synchronized boolean verifyTrialBalance() {
long totalDebits = 0;
long totalCredits = 0;
for (JournalEntry entry : journal) {
totalDebits += entry.getAmountCents();
totalCredits += entry.getAmountCents();
}
return totalDebits == totalCredits;
}
}
public class Main {
public static void main(String[] args) {
System.out.println("=== CORE BANKING LEDGER SIMULATION ===");
BankingLedgerSystem ledger = new BankingLedgerSystem();
// 1. Create accounts
Account cash = new Account("acc-cash", "Cash Reserve", AccountType.ASSET, "USD");
Account deposits = new Account("acc-deposits", "Customer Deposits", AccountType.LIABILITY, "USD");
Account revenue = new Account("acc-revenue", "Service Fees", AccountType.INCOME, "USD");
ledger.createAccount(cash);
ledger.createAccount(deposits);
ledger.createAccount(revenue);
System.out.println("Accounts successfully created: Cash (ASSET), Deposits (LIABILITY), Revenue (INCOME).");
// 2. Post transactions (Initial deposits and fees)
System.out.println("\nPosting initial transactions...");
JournalEntry entry1 = ledger.postTransaction("ref-init-100", "acc-cash", "acc-deposits", 500000, "Initial client cash deposit");
System.out.println("Posted Entry ID: " + entry1.getEntryId() + " | Ref: " + entry1.getReferenceId() + " | Amount: $5000.00");
JournalEntry entry2 = ledger.postTransaction("ref-init-200", "acc-cash", "acc-revenue", 15000, "Account creation service charge");
System.out.println("Posted Entry ID: " + entry2.getEntryId() + " | Ref: " + entry2.getReferenceId() + " | Amount: $150.00");
// 3. Verify Balances
Instant now = Instant.now();
System.out.println("\nChecking balances as of now:");
System.out.println("Cash Account Balance: $" + (ledger.getBalance("acc-cash", now) / 100.0));
System.out.println("Deposits Account Balance: $" + (ledger.getBalance("acc-deposits", now) / 100.0));
System.out.println("Revenue Account Balance: $" + (ledger.getBalance("acc-revenue", now) / 100.0));
// 4. Test Idempotency
System.out.println("\nTesting Idempotency on duplicate Reference ID (ref-init-100)...");
try {
ledger.postTransaction("ref-init-100", "acc-cash", "acc-deposits", 500000, "Duplicate attempt");
System.out.println("WARNING: Duplicate transaction succeeded incorrectly!");
} catch (IllegalStateException e) {
System.out.println("SUCCESS: Duplicate transaction blocked as expected. Message: " + e.getMessage());
}
// 5. Post Reversal
System.out.println("\nReversing the service charge transaction (ref-init-200)...");
JournalEntry reversal = ledger.postReversal("ref-init-200", "ref-rev-200");
System.out.println("Reversal Entry ID: " + reversal.getEntryId() + " | Ref: " + reversal.getReferenceId() + " | Amount: $150.00");
System.out.println("Updated Cash Account Balance: $" + (ledger.getBalance("acc-cash", Instant.now()) / 100.0));
System.out.println("Updated Revenue Account Balance: $" + (ledger.getBalance("acc-revenue", Instant.now()) / 100.0));
// 6. Verify Trial Balance Invariant
System.out.println("\nVerifying global Trial Balance (Debit == Credit) invariant...");
boolean isBalanced = ledger.verifyTrialBalance();
System.out.println("Ledger is in balance: " + isBalanced);
}
}