Machine Coding Problem

Banking Ledger

maco30maco60macoAllfintechdouble-entryimmutability
Commonly Asked By:StripeBlockRevolutJPMorgan Chase

Requirements & Scope

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.

Ledger Layout Architecture

Relationship mapping of Accounts, Journal entries, Invariant trial balances, and Idempotency key stores:

Loading...

Fintech Core Invariants

  • Normal Balance Type Rule: ASSET and EXPENSE use Debits - Credits, whereas LIABILITY, EQUITY, and INCOME use Credits - Debits.
  • Trial Balance Verification Invariant: Validates system health by proving SUM(debits) === SUM(credits) across all ledger logs.
  • Audit Log immutability: Transactions are sequentially logged with absolute timestamps, providing full regulatory visibility.

Execution Workflows

Atomicity & Idempotency Flow

  1. Verify Idempotency: Check whether the transaction referenceId exists. Block duplicates immediately.
  2. Assert Invariants: Ensure that both source and destination accounts are active and trade using matching currency codes.
  3. Commit Transaction Pair: Log the immutable double-entry item into the append-only registry.
  4. Assert Invariant Balance Sheet: Trigger audit sweeps to verify the dynamic Trial Balance equation stays matching.

Concurrency & Thread Safety Strategy

Enforcing strict consistency, double-spend prevention, and deadlock-free operation under heavy simultaneous transactions:

  • Pessimistic Row-Level Locking: To prevent race conditions during balance validation (such as concurrent overdrafts on the same account), the ledger acquires pessimistic database locks (e.g., SELECT FOR UPDATE) on affected account records.
  • Deadlock Avoidance Strategy: Multi-row account updates must always acquire locks in a globally deterministic, sorted order (e.g., ORDER BY account_id ASC). This guarantees that concurrent transfers between the same two accounts never cause cyclic dependency deadlocks.
  • Atomic Double-Entry Transactions: To prevent split-brain entries where a debit succeeds but the corresponding credit fails, the posting of journal entries and balance updates are committed inside a strict database transaction wrapper supporting ACID compliance.
  • Idempotency Keys with Unique Indexes: Retried client requests must never result in duplicate postings. Using a unique database index on the referenceId (or idempotency key) ensures that concurrent retries fail at the database constraint level.

Clean Code Blueprint

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);
    }
}

Interactive Simulator

📊 Core Double-Entry Ledger Simulator

Observe atomic ledger balances, immutable sequential journal registers, instant transaction reversals, and real-time trial balance verification.

📝 Post Double-Entry Journal Entry

Debit Account (Dr):
Credit Account (Cr):
Amount (USD):
Reference ID (Idempotency Key):
Description:

🕰️ Ledger Historical Time-TravelerEntry 2 of 2

Drag the timeline bar to reconstruct exact bank account state values as of past committed transaction sequences.

📋 Dynamic Account Balance Sheet

Cash Reserve (Asset)$5,150.00Type: ASSET | normal Debit
Customer Deposits (Liability)$5,000.00Type: LIABILITY | normal Credit
Service Fees (Income)$150.00Type: INCOME | normal Credit
Operating Rent (Expense)$0.00Type: EXPENSE | normal Debit

⚖️ Trial Balance Invariant

Asserts Sum(Debits) === Sum(Credits) globally
Debits: $5,150.00 | Credits: $5,150.00
Balanced Invariant Verified

📂 Immutable Append-Only Ledger Journal (2 Entries)

Entry IDRef ID (Idempotency)Debit Account (Dr)Credit Account (Cr)AmountDescriptionActions
ent-1ref-init-100acc-cashacc-deposits$5,000.00Initial client cash deposit
ent-2ref-init-200acc-cashacc-revenue$150.00Account creation service charge
Audit & Balance Verification Logs
[14:10:00] Banking ledger operational. Double-entry engine configured.
[14:10:01] Initial balances posted. Global debit/credit invariants checked.

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...