Implement a Clean In-Memory Transaction Ledger
RampSoftware Engineer · OnsiteLast 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 wherefrom_account_id == to_account_id), or atx_idcollision occurred where the giventx_idwas 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
0cents 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?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.