Machine Coding Problem

Banking Ledger

maco30maco60macoAllfintechdouble-entryimmutability
Commonly Asked By:StripeBlockRevolutJPMorgan Chase

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