Design a bank system with scheduled transfers
Company: Meta
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
Quick Answer: This question evaluates object-oriented design and stateful system skills, including time-ordered event processing, scheduling and cancellation semantics, balance accounting, and top-k aggregation using appropriate data structures.
Constraints
- `1 <= len(operations) <= 10^5`
- Timestamps are integers in non-decreasing order.
- `customer_id`, `from_id`, and `to_id` are non-empty strings and IDs are never reused after creation.
- Amounts are positive integers; `delay` and `n` are non-negative integers.
Examples
Input: [('create_account', 1, 'alice'), ('create_account', 1, 'bob'), ('deposit', 2, 'alice', 100), ('transfer', 3, 'alice', 'bob', 30), ('withdraw', 4, 'bob', 10), ('top_spending', 5, 2)]
Expected Output: [True, True, True, True, True, ['alice(30)', 'bob(10)']]
Explanation: Alice sends 30 to Bob, and Bob withdraws 10. Spending totals are Alice=30 and Bob=10.
Input: [('create_account', 1, 'ann'), ('create_account', 1, 'ben'), ('deposit', 2, 'ann', 50), ('schedule_transfer', 3, 'ann', 'ben', 20, 5), ('top_spending', 4, 2), ('cancel_transfer', 7, 'ann', 'transfer1'), ('top_spending', 9, 2)]
Expected Output: [True, True, True, 'transfer1', ['ann(0)', 'ben(0)'], True, ['ann(0)', 'ben(0)']]
Explanation: The scheduled transfer reserves 20 immediately, but spending stays 0 until execution. It is canceled before time 8, so the money is refunded and no spending is added.
Input: [('create_account', 1, 'A'), ('create_account', 1, 'B'), ('create_account', 1, 'C'), ('deposit', 2, 'A', 100), ('schedule_transfer', 5, 'A', 'B', 40, 5), ('merge_account', 6, 'B', 'C'), ('merge_account', 7, 'A', 'C'), ('top_spending', 10, 3)]
Expected Output: [True, True, True, True, 'transfer1', True, True, ['C(0)']]
Explanation: The pending transfer originally A->B becomes C->C after both merges. At time 10 it executes by returning the reserved 40 to C, so there is no net transfer out and spending remains 0.
Input: [('create_account', 1, 'x'), ('deposit', 1, 'x', 10), ('schedule_transfer', 1, 'x', 'x', 5, 0), ('top_spending', 1, 1), ('create_account', 2, 'x'), ('withdraw', 3, 'y', 1), ('schedule_transfer', 4, 'x', 'missing', 1, 1)]
Expected Output: [True, True, 'transfer1', ['x(0)'], False, False, None]
Explanation: The zero-delay transfer is not executed during its own scheduling call; it is processed before the next operation at the same timestamp. Duplicate creation fails, withdrawing from a missing account fails, and scheduling to a missing receiver returns None.
Solution
def solution(operations):
import heapq
balances = {}
spending = {}
version = {}
parent = {}
all_ids = set()
scheduled_heap = []
spend_heap = []
transfers = {}
next_transfer_num = 1
schedule_seq = 0
results = []
def find(x):
path = []
while x in parent:
path.append(x)
x = parent[x]
for node in path:
parent[node] = x
return x
def push_spending(customer_id):
heapq.heappush(spend_heap, (-spending[customer_id], customer_id, version[customer_id]))
def process_due(current_time):
while scheduled_heap and scheduled_heap[0][0] <= current_time:
_, _, transfer_id = heapq.heappop(scheduled_heap)
rec = transfers.get(transfer_id)
if rec is None or rec['status'] != 'pending':
continue
sender = find(rec['from'])
receiver = find(rec['to'])
amount = rec['amount']
if sender == receiver:
balances[sender] += amount
else:
balances[receiver] += amount
spending[sender] += amount
version[sender] += 1
push_spending(sender)
rec['status'] = 'executed'
for op in operations:
name = op[0]
timestamp = op[1]
process_due(timestamp)
if name == 'create_account':
_, _, customer_id = op
if customer_id in all_ids:
results.append(False)
else:
all_ids.add(customer_id)
balances[customer_id] = 0
spending[customer_id] = 0
version[customer_id] = 0
push_spending(customer_id)
results.append(True)
elif name == 'deposit':
_, _, customer_id, amount = op
if customer_id not in balances:
results.append(False)
else:
balances[customer_id] += amount
results.append(True)
elif name == 'withdraw':
_, _, customer_id, amount = op
if customer_id not in balances or balances[customer_id] < amount:
results.append(False)
else:
balances[customer_id] -= amount
spending[customer_id] += amount
version[customer_id] += 1
push_spending(customer_id)
results.append(True)
elif name == 'transfer':
_, _, from_id, to_id, amount = op
if from_id not in balances or to_id not in balances or balances[from_id] < amount:
results.append(False)
elif from_id == to_id:
results.append(True)
else:
balances[from_id] -= amount
balances[to_id] += amount
spending[from_id] += amount
version[from_id] += 1
push_spending(from_id)
results.append(True)
elif name == 'top_spending':
_, _, n = op
taken = []
answer = []
while len(answer) < n and spend_heap:
neg_total, customer_id, ver = heapq.heappop(spend_heap)
if customer_id not in balances:
continue
if ver != version[customer_id]:
continue
if -neg_total != spending[customer_id]:
continue
taken.append((neg_total, customer_id, ver))
answer.append(f'{customer_id}({spending[customer_id]})')
for item in taken:
heapq.heappush(spend_heap, item)
results.append(answer)
elif name == 'schedule_transfer':
_, _, from_id, to_id, amount, delay = op
if from_id not in balances or to_id not in balances or balances[from_id] < amount:
results.append(None)
else:
transfer_id = f'transfer{next_transfer_num}'
next_transfer_num += 1
schedule_seq += 1
balances[from_id] -= amount
transfers[transfer_id] = {
'from': from_id,
'to': to_id,
'amount': amount,
'status': 'pending'
}
heapq.heappush(scheduled_heap, (timestamp + delay, schedule_seq, transfer_id))
results.append(transfer_id)
elif name == 'cancel_transfer':
_, _, customer_id, transfer_id = op
rec = transfers.get(transfer_id)
if customer_id not in balances or rec is None or rec['status'] != 'pending':
results.append(False)
else:
current_sender = find(rec['from'])
if current_sender != customer_id:
results.append(False)
else:
balances[current_sender] += rec['amount']
rec['status'] = 'canceled'
results.append(True)
elif name == 'merge_account':
_, _, from_id, to_id = op
if from_id not in balances or to_id not in balances or from_id == to_id:
results.append(False)
else:
balances[to_id] += balances[from_id]
spending[to_id] += spending[from_id]
version[to_id] += 1
push_spending(to_id)
parent[from_id] = to_id
del balances[from_id]
del spending[from_id]
del version[from_id]
results.append(True)
else:
raise ValueError('Unknown operation')
return resultsExplanation
Time complexity: O((q + r) log q), where q is the number of operations and r is the total number of names returned across all top_spending calls. Each op does O(log q) heap work; top_spending pops/repushes one entry per returned name (stale entries are discarded permanently, amortizing to O(log q) each); union-find find is near-O(1) amortized with path compression.. Space complexity: O(q) — the dicts hold at most one entry per account, and both heaps plus the transfers map grow at most linearly in the number of operations..
Hints
- A min-heap keyed by execution time is a natural way to process all scheduled transfers due before the current operation.
- Do not scan every pending transfer during `merge_account`. Instead, keep a mapping from old IDs to current live IDs and resolve them lazily when a scheduled transfer executes or is canceled.