Katkat

Implement a Clean In-Memory Transaction Ledger

RampSoftware Engineer · Onsite

Last updated

Design an in-memory double-entry style financial transaction ledger that supports atomic transfers, strict balance invariants, and idempotency key handling.

Problem Statement

Financial platforms require reliable record-keeping that guarantees consistency, prevents balance overdrafts, and supports safe network retries. Design and implement an in-memory transaction ledger class, Ledger, that manages account balances using integer cents.

Your ledger must support deposits, withdrawals, transfers, and balance queries. It must also handle idempotency: if a client retries an operation using the exact same tx_id and identical parameters, the ledger must return the previous result without applying balance changes a second time. If a client attempts to reuse a tx_id with different parameters, the operation must be rejected as an invalid conflict.

Class Interface

Implement the following class and methods:

class Ledger:
    def __init__(self):
        pass

    def deposit(self, tx_id: str, account_id: str, amount_cents: int) -> str:
        pass

    def withdraw(self, tx_id: str, account_id: str, amount_cents: int) -> str:
        pass

    def transfer(self, tx_id: str, from_account_id: str, to_account_id: str, amount_cents: int) -> str:
        pass

    def get_balance(self, account_id: str) -> int:
        pass

Return Values

Mutating operations (deposit, withdraw, transfer) must return one of the following string status codes:

  • "SUCCESS": The operation succeeded, or is an exact retry of a previously successful operation.
  • "INSUFFICIENT_FUNDS": The source account does not have enough funds to complete the withdrawal or transfer.
  • "INVALID_TRANSACTION": Input validation failed (e.g. amount_cents <= 0, transfer where from_account_id == to_account_id), or a tx_id collision occurred where the given tx_id was previously used with different parameters.

get_balance should return the current balance of the account in integer cents. Accounts not yet referenced default to a balance of 0.

Examples

ledger = Ledger()

# Standard operations
ledger.deposit("tx_1", "acc_A", 1000)  # -> "SUCCESS"
ledger.get_balance("acc_A")             # -> 1000

# Overdraft attempt
ledger.withdraw("tx_2", "acc_A", 1500) # -> "INSUFFICIENT_FUNDS"
ledger.get_balance("acc_A")             # -> 1000

# Idempotent retry (exact same params)
ledger.deposit("tx_1", "acc_A", 1000)  # -> "SUCCESS"
ledger.get_balance("acc_A")             # -> 1000 (not 2000)

# tx_id conflict (same tx_id, different params)
ledger.deposit("tx_1", "acc_A", 500)   # -> "INVALID_TRANSACTION"

# Atomic transfer
ledger.transfer("tx_3", "acc_A", "acc_B", 400) # -> "SUCCESS"
ledger.get_balance("acc_A")                      # -> 600
ledger.get_balance("acc_B")                      # -> 400

Constraints

  • amount_cents > 0 for all operations.
  • Account IDs and transaction IDs are non-empty strings.
  • All accounts implicitly start with 0 cents balance.
  • Operations must be atomic: if a transfer fails due to insufficient funds, neither account's balance should change.

Hints

Hint 1

Store each transaction by its `tx_id` along with its normalized request signature (operation type, accounts, amount) and the resulting status.

Hint 2

When a transaction request arrives, check whether `tx_id` exists before checking balances; compare the incoming request payload with the stored payload.

Hint 3

Represent transfers as an atomic pair of debit and credit operations; ensure you verify the sender's balance before applying either mutation.

Follow-up questions

  • How would you ensure thread safety if multiple worker threads access the ledger concurrently without creating a global lock bottleneck?
  • How would you evolve this data model into an append-only, double-entry bookkeeping system to support audit logs and transaction rollbacks?
  • How would you handle idempotency key expiration in a high-throughput production environment without exhausting memory?