Extend a Banking Interface with Scheduled Payments and Account Merges
CoinbaseSoftware Engineer · Technical ScreenLast updated
Implement an in-memory transactional banking system supporting scheduled payments, deterministic tick-based execution, idempotency, and account merging with balance consolidation and redirect tracking.
Problem Statement
You are tasked with extending an in-memory banking ledger to support scheduled payments and account merges. All monetary values are represented as non-negative integers (cents).
Implement the BankingSystem class with the following methods:
class BankingSystem:
def __init__(self):
pass
def create_account(self, account_id: str, initial_balance: int) -> bool:
pass
def schedule_payment(self, payment_id: str, timestamp: int, from_account: str, to_account: str, amount: int) -> bool:
pass
def cancel_payment(self, payment_id: str) -> bool:
pass
def merge_accounts(self, source_account: str, target_account: str) -> bool:
pass
def advance_time(self, current_time: int) -> list[dict]:
pass
def get_balance(self, account_id: str) -> int:
pass
Requirements and Rules
- Account Creation:
create_accountcreates an account withinitial_balance >= 0. ReturnsFalseifaccount_idalready exists or was previously merged, otherwiseTrue. - Account Merging:
merge_accounts(source, target)consolidatessourceintotarget.sourceis permanently retired. Its entire current balance is added totarget. Ifsourcewas already retired, ortargetis retired/does not exist, returnFalse. Ifsource == target, returnFalse.- Any existing scheduled payments involving
source(as sender or receiver) that execute in the future must automatically be re-routed to usetarget. - Any chain of merges must be resolved (e.g., if A merges into B, and B later merges into C, transactions originally targeting A now route to C).
- Returns
Trueon success.
- Scheduled Payments & Idempotency:
schedule_paymentschedules a payment to execute attimestamp.- If
payment_idwas already registered (regardless of whether it executed, failed, or was cancelled), returnFalse. - If
from_accountorto_accountdoes not exist (or resolves to the exact same account after merge resolution), returnFalse. amountmust be greater than0.
- Cancellation:
cancel_paymentcancels a payment if it exists and has not yet executed. ReturnsTrueif cancelled,Falseif the payment does not exist or has already executed/failed/cancelled. - Execution (
advance_time):- Processes all pending scheduled payments with
payment.timestamp <= current_time. - Deterministic Order: Payments with smaller timestamps execute first. For payments with identical timestamps, order alphabetically (ascending) by
payment_id. - Execution Logic: For each payment, resolve
from_accountandto_accountto their active target accounts. Iffrom_accountbalance is< amount, the payment fails (status"FAILED") and no money moves. Otherwise, deductamountfromfrom_accountand addamounttoto_account(status"SUCCESS"). - Returns a list of dicts in execution order:
{"payment_id": str, "status": "SUCCESS" | "FAILED", "timestamp": int}.
- Processes all pending scheduled payments with
- Balance Query:
get_balancereturns the current balance of the resolved active account. If the account does not exist and was never created, return-1.
Example
bank = BankingSystem()
bank.create_account("acc1", 100)
bank.create_account("acc2", 50)
bank.create_account("acc3", 0)
# Schedule payments
bank.schedule_payment("p2", 10, "acc1", "acc2", 60) # timestamp 10
bank.schedule_payment("p1", 10, "acc1", "acc3", 50) # timestamp 10
# At timestamp 10, p1 executes before p2 due to lexicographical order of IDs
# p1 succeeds (acc1: 100 -> 50, acc3: 0 -> 50)
# p2 then attempts to transfer 60 from acc1, but acc1 only has 50 -> Fails
results = bank.advance_time(10)
# results == [
# {"payment_id": "p1", "status": "SUCCESS", "timestamp": 10},
# {"payment_id": "p2", "status": "FAILED", "timestamp": 10}
# ]
# Merge acc3 into acc2
bank.merge_accounts("acc3", "acc2")
# acc3 balance (50) is merged into acc2 (50 + 50 = 100)
assert bank.get_balance("acc2") == 100
assert bank.get_balance("acc3") == 100 # acc3 resolves to acc2
Hints
Hint 1
Use a disjoint-set (union-find) structure or redirect pointer map with path compression to resolve merged accounts to their active canonical destination.
Hint 2
Store pending payments in a min-heap keyed by `(timestamp, payment_id)` to ensure deterministic execution ordering.
Hint 3
Track the lifecycle states of payments (`PENDING`, `CANCELLED`, `SUCCESS`, `FAILED`) in an idempotency registry map so re-used IDs are rejected.
Follow-up questions
- How would you ensure crash recovery and replayability if the system crashed mid-way through `advance_time`?
- If millions of payments are scheduled concurrently across distributed nodes, how would you partition accounts while guaranteeing atomic merge operations and balance invariants without a global lock?
- How would you support recurring payments without bloating the priority queue with infinite future events?
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.