Katkat

Design a Stateful Banking System with Scheduled Payments and Account Merges

AirbnbSoftware Engineer · Online Assessment

Last 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 of 0. Returns True if created successfully, or False if account_id already exists.

  • deposit(timestamp: int, account_id: str, amount: int) -> int | None: Deposits amount into account_id. Returns the new balance, or None if the account does not exist.

  • transfer(timestamp: int, src_id: str, dest_id: str, amount: int) -> bool: Transfers amount from src_id to dest_id. Returns True if both accounts exist, src_id != dest_id, and src_id has balance >= amount. Otherwise, returns False. A successful transfer increases the total outgoing spending of src_id by amount.

  • top_spenders(timestamp: int, n: int) -> list[str]: Returns up to n account IDs with the highest total outgoing transfer amounts, sorted in descending order of spending. Ties must be broken lexicographically by account_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 at timestamp + delay. Returns True if payment_id is unique, both accounts currently exist, src_id != dest_id, and amount > 0. Otherwise, returns False.

  • cancel_payment(timestamp: int, payment_id: str) -> bool: Cancels a pending payment. Returns True if the payment exists and has not yet executed. Returns False if payment_id is unknown, already cancelled, or already executed.

  • merge_accounts(timestamp: int, target_id: str, source_id: str) -> bool: Merges source_id into target_id. Returns False if target_id == source_id or either account does not exist. Otherwise, transfers all funds and historical outgoing spending from source_id into target_id, reassigns all pending scheduled payments referencing source_id (as source or destination) to target_id, deactivates source_id, and returns True.

Scheduled Payment Processing Rules

  1. At the start of any call with time t, process all pending, non-cancelled payments with execution_time <= t.
  2. Payments must be processed in ascending order of execution_time. If two payments share the same execution time, break ties lexicographically by payment_id.
  3. When executing a payment, verify that the current src_id has sufficient balance (>= amount) and that src_id != dest_id. If valid, transfer the funds and increase src_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^9 with 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?