Build a Progressive Banking Ledger
AnthropicSoftware Engineer · OnsiteLast updated
Design and implement an in-memory, timestamped banking ledger supporting atomic transfers, cashback, scheduled payments, account merges with alias resolution, and historical balance lookups.
Problem Statement
Implement an in-memory banking ledger class, BankLedger, that processes financial transactions deterministically over time. Operations are submitted with monotonically non-decreasing timestamps.
Your ledger must support the following methods:
-
create_account(account_id: str) -> bool: Creates an account with an initial balance of0. ReturnsFalseifaccount_idalready exists or has been merged into another account; otherwise creates it and returnsTrue. -
deposit(timestamp: int, account_id: str, amount: int) -> int | None: Depositsamountintoaccount_id. Returns the updated balance, orNoneif the account does not exist oramount bool: Transfersamountfromsource_idtotarget_id. Ifsource_idhas sufficient funds (balance >= amount) andamount > 0, deductsamountfromsource_id, creditsamounttotarget_id, and immediately credits a2%cashback ((amount * 2) // 100) back tosource_id. ReturnsTrueon success, orFalseif either account does not exist, accounts resolve to the same account, or funds are insufficient. -
schedule_transfer(timestamp: int, transfer_id: str, source_id: str, target_id: str, amount: int, execute_at: int) -> bool: Schedules a transfer to run atexecute_at(execute_at > timestamp). ReturnsFalseiftransfer_idalready exists. The balance is not checked or reserved until execution time. ReturnsTrueif scheduled. -
merge_accounts(timestamp: int, primary_id: str, secondary_id: str) -> bool: Mergessecondary_idintoprimary_id. All funds fromsecondary_idmove toprimary_id.secondary_idbecomes an alias ofprimary_id: any subsequent operation targetingsecondary_idmust resolve toprimary_id. ReturnsFalseif either account does not exist or they already resolve to the same account; otherwise returnsTrue. -
get_balance(timestamp: int, account_id: str, at_time: int | None = None) -> int | None: Returns the balance ofaccount_idat timestampat_time(or current balance attimestampifat_time is None). Ifat_timeis specified, it reflects the balance immediately after all transactions at or beforeat_timecompleted. ReturnsNoneifaccount_iddoes not exist. -
get_top_spenders(timestamp: int, k: int) -> list[str]: Returns the topkaccount IDs ranked by total gross outgoing transfer amount (excluding cashback deductions). Ties are broken lexicographically by canonicalaccount_id.
Deterministic Event Processing Rules
- Before processing any method called at
timestamp, the ledger must execute all scheduled transfers withexecute_at <= timestampin ascending order ofexecute_at. If multiple transfers share the sameexecute_at, execute them in lexicographical order oftransfer_id. - If a scheduled transfer fails due to insufficient funds at execution time, it is discarded and does not retry.
Example
ledger = BankLedger()
ledger.create_account("alice") # True
ledger.create_account("bob") # True
ledger.deposit(1, "alice", 1000) # 1000
ledger.schedule_transfer(2, "tx1", "alice", "bob", 500, 10) # True
ledger.transfer(5, "alice", "bob", 200) # True
# Alice sent 200, received 4 cashback (200 * 0.02). Alice balance = 804. Bob = 200.
ledger.get_balance(10, "alice")
# Before returning balance at timestamp 10, "tx1" executes.
# Alice sends 500, gets 10 cashback. Alice balance = 804 - 500 + 10 = 314. Bob = 700.
# Returns 314
ledger.get_top_spenders(10, 1) # ["alice"] (gross spent: 200 + 500 = 700)
ledger.merge_accounts(12, "bob", "alice") # True: Alice merges into Bob
# Bob now has 700 + 314 = 1014. Alice is an alias for Bob.
ledger.deposit(15, "alice", 100) # 1114 (deposited into Bob via alias)
ledger.get_balance(15, "bob") # 1114
ledger.get_balance(15, "alice", at_time=1) # 1000 (historical balance of Alice at t=1)
Constraints
1 = timestamp_{i-1}).
Hints
Hint 1
Process scheduled events lazily: use a min-heap keyed by `(execute_at, transfer_id)` and drain it up to the current timestamp before executing any operation.
Hint 2
Use a Disjoint Set (Union-Find) or a persistent alias pointer map to resolve accounts to their canonical IDs while keeping historical records intact under their original IDs.
Hint 3
For historical balance queries, record `(timestamp, balance)` snapshots for each account whenever its balance changes, then use binary search (`bisect`) to answer queries for `at_time`.
Follow-up questions
- How would you handle recurring scheduled transfers (e.g., every N days) without unbounded memory growth?
- If timestamps could arrive out of order (distributed network delays), how would your event-sourcing model need to change to support rollbacks or compensating transactions?
- How would you scale this design across multiple shards when transfers frequently cross partition boundaries?
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.