Design a Stateful Banking System with Scheduled Payments and Account Merges
AirbnbSoftware Engineer · Online AssessmentLast updated
Design an in-memory stateful banking system that supports account creation, transfers, spending leaderboards, scheduled payments with deterministic backlog processing, and account merges.
Problem Statement
Design an in-memory banking system that manages bank accounts, fund transfers, top-spender analytics, scheduled transactions, and account merges.
All operations receive a non-decreasing integer timestamp. Before any operation is executed at time t, the system must process all pending scheduled payments whose execution time is less than or equal to t in deterministic order.
Implement the BankSystem class with the following methods:
-
create_account(timestamp: int, account_id: str) -> bool: Creates an account with an initial balance of0. ReturnsTrueif created successfully, orFalseifaccount_idalready exists. -
deposit(timestamp: int, account_id: str, amount: int) -> int | None: Depositsamountintoaccount_id. Returns the new balance, orNoneif the account does not exist. -
transfer(timestamp: int, src_id: str, dest_id: str, amount: int) -> bool: Transfersamountfromsrc_idtodest_id. ReturnsTrueif both accounts exist,src_id != dest_id, andsrc_idhas balance>= amount. Otherwise, returnsFalse. A successful transfer increases the total outgoing spending ofsrc_idbyamount. -
top_spenders(timestamp: int, n: int) -> list[str]: Returns up tonaccount IDs with the highest total outgoing transfer amounts, sorted in descending order of spending. Ties must be broken lexicographically byaccount_id. Only currently active accounts are included. -
schedule_payment(timestamp: int, payment_id: str, src_id: str, dest_id: str, amount: int, delay: int) -> bool: Schedules a payment to execute attimestamp + delay. ReturnsTrueifpayment_idis unique, both accounts currently exist,src_id != dest_id, andamount > 0. Otherwise, returnsFalse. -
cancel_payment(timestamp: int, payment_id: str) -> bool: Cancels a pending payment. ReturnsTrueif the payment exists and has not yet executed. ReturnsFalseifpayment_idis unknown, already cancelled, or already executed. -
merge_accounts(timestamp: int, target_id: str, source_id: str) -> bool: Mergessource_idintotarget_id. ReturnsFalseiftarget_id == source_idor either account does not exist. Otherwise, transfers all funds and historical outgoing spending fromsource_idintotarget_id, reassigns all pending scheduled payments referencingsource_id(as source or destination) totarget_id, deactivatessource_id, and returnsTrue.
Scheduled Payment Processing Rules
- At the start of any call with time
t, process all pending, non-cancelled payments withexecution_time <= t. - Payments must be processed in ascending order of
execution_time. If two payments share the same execution time, break ties lexicographically bypayment_id. - When executing a payment, verify that the current
src_idhas sufficient balance (>= amount) and thatsrc_id != dest_id. If valid, transfer the funds and increasesrc_id's outgoing total. If invalid (e.g., insufficient balance or source merged into destination), the payment fails silently and is discarded.
Example
bank = BankSystem()
bank.create_account(1, "A") # returns True
bank.create_account(1, "B") # returns True
bank.deposit(2, "A", 100) # returns 100
bank.schedule_payment(3, "p1", "A", "B", 40, 5) # returns True, runs at t=8
bank.transfer(4, "A", "B", 20) # returns True (A: 80, B: 20)
bank.top_spenders(5, 2) # returns ["A", "B"] (A spent 20, B spent 0)
bank.deposit(8, "B", 10) # triggers p1 at t=8; A balance becomes 40, B becomes 70; returns 70
bank.top_spenders(9, 1) # returns ["A"] (A spent 60)
Constraints
1 <= timestamp <= 10^9with timestamps non-decreasing across consecutive method calls.1 <= amount, delay <= 10^9- Account and payment IDs consist of alphanumeric characters (
1 <= len(id) <= 32). - Total number of method calls across all operations
<= 10^4.
Hints
Hint 1
Use a priority queue (min-heap) ordered by (execution_time, payment_id) to greedily process all due payments at the beginning of every method invocation.
Hint 2
Handle account merges by maintaining an indirection map or Union-Find structure so that any pending payment dynamically resolves to the canonical target account.
Hint 3
Track each account's lifetime outgoing transfer sum as an integer field updated on each successful direct or scheduled transfer, avoiding full-history scans for `top_spenders`.
Follow-up questions
- How would you ensure thread-safety and linearizability if methods are invoked concurrently across multiple distributed API workers?
- If failed scheduled payments required automatic exponential backoff retries instead of immediate failure, how would you adapt your scheduling queue?
- How would you design the system to support point-in-time balance and audit queries (e.g., 'What was account A's balance at timestamp T?') without replaying all operations?
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.