Design an in-memory banking service
Company: Anthropic
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates system-design and algorithmic competencies related to stateful in-memory services, including temporal semantics, merge and re-creation semantics, transactional transfer handling, activity aggregation, and appropriate data-structure and complexity reasoning.
Part 1: Reusable Account IDs After Merge-Away
Constraints
- 1 <= len(operations) <= 200000
- Account IDs are non-empty strings of length at most 20
- Timestamps are integers in non-decreasing order
Examples
Input: ([('create', 'A', 1), ('create', 'A', 2)],)
Expected Output: [True, False]
Explanation: The second create fails because A is still active.
Input: ([('create', 'A', 1), ('merge_away', 'A', 5), ('create', 'A', 7)],)
Expected Output: [True, True, True]
Explanation: After A is merged away, the ID can be reused.
Input: ([('merge_away', 'X', 1), ('create', 'X', 2)],)
Expected Output: [False, True]
Explanation: You cannot merge away an inactive ID, but you may create it afterward.
Input: ([('create', 'A', 1), ('create', 'B', 2), ('merge_away', 'A', 3), ('create', 'A', 4), ('merge_away', 'B', 5), ('merge_away', 'B', 6)],)
Expected Output: [True, True, True, True, True, False]
Explanation: The last operation fails because B was already merged away.
Solution
def solution(operations):
active = set()
result = []
for op in operations:
kind = op[0]
if kind == 'create':
_, acc_id, _ = op
if acc_id in active:
result.append(False)
else:
active.add(acc_id)
result.append(True)
elif kind == 'merge_away':
_, acc_id, _ = op
if acc_id in active:
active.remove(acc_id)
result.append(True)
else:
result.append(False)
else:
raise ValueError('Unknown operation type')
return resultTime complexity: O(n) average time, where n is the number of operations. Space complexity: O(m), where m is the number of currently active IDs.
Hints
- You only need to know whether an ID is currently active or not.
- A successful `merge_away` frees that ID for a later `create`.
Part 2: Timestamped Deposits and Payments
Constraints
- 1 <= len(operations) <= 200000
- 0 <= amount <= 10^9
- Timestamps are integers in non-decreasing order
Examples
Input: ([('create', 'A', 1), ('deposit', 'A', 100, 2), ('pay', 'A', 30, 3)],)
Expected Output: [True, 100, 70]
Explanation: A is created, then updated twice.
Input: ([('create', 'A', 1), ('pay', 'A', 1, 2)],)
Expected Output: [True, None]
Explanation: The payment is rejected because A has insufficient funds.
Input: ([('deposit', 'X', 10, 1), ('create', 'X', 2), ('pay', 'X', 0, 3)],)
Expected Output: [None, True, 0]
Explanation: The first deposit is invalid because X does not exist yet. A zero payment on balance 0 succeeds.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 50, 2), ('deposit', 'B', 20, 2), ('pay', 'A', 50, 2), ('pay', 'B', 25, 3)],)
Expected Output: [True, True, 50, 20, 0, None]
Explanation: A can pay exactly its balance, but B cannot overdraw.
Solution
def solution(operations):
balances = {}
result = []
for op in operations:
kind = op[0]
if kind == 'create':
_, acc_id, _ = op
if acc_id in balances:
result.append(False)
else:
balances[acc_id] = 0
result.append(True)
elif kind == 'deposit':
_, acc_id, amount, _ = op
if acc_id not in balances:
result.append(None)
else:
balances[acc_id] += amount
result.append(balances[acc_id])
elif kind == 'pay':
_, acc_id, amount, _ = op
if acc_id not in balances or balances[acc_id] < amount:
result.append(None)
else:
balances[acc_id] -= amount
result.append(balances[acc_id])
else:
raise ValueError('Unknown operation type')
return resultTime complexity: O(n) average time. Space complexity: O(m), where m is the number of existing accounts.
Hints
- A hash map from account ID to current balance is enough for this sub-problem.
- Treat amount 0 like a normal operation; it still succeeds if the account exists.
Part 3: Pending Transfers and Acceptance
Constraints
- 1 <= len(operations) <= 200000
- 0 <= amount <= 10^9
- Timestamps are integers in non-decreasing order
Examples
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 100, 2), ('transfer', 'A', 'B', 40, 3), ('balance', 'A', 4), ('accept', 'B', 1, 5), ('balance', 'B', 6)],)
Expected Output: [True, True, 100, 1, 60, True, 40]
Explanation: The transfer debits A immediately and credits B only on acceptance.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 20, 2), ('transfer', 'A', 'B', 30, 3), ('balance', 'A', 4), ('accept', 'B', 1, 5)],)
Expected Output: [True, True, 20, None, 20, False]
Explanation: The transfer fails because A does not have enough money.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('create', 'C', 1), ('deposit', 'A', 50, 2), ('transfer', 'A', 'B', 20, 3), ('accept', 'C', 1, 4), ('balance', 'B', 5), ('accept', 'B', 1, 6), ('balance', 'B', 7)],)
Expected Output: [True, True, True, 50, 1, False, 0, True, 20]
Explanation: The wrong destination cannot accept the transfer, but the correct destination still can later.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 0, 2), ('transfer', 'A', 'B', 0, 3), ('accept', 'B', 1, 4), ('accept', 'B', 1, 5), ('balance', 'A', 6), ('balance', 'B', 6)],)
Expected Output: [True, True, 0, 1, True, False, 0, 0]
Explanation: A zero-amount transfer is allowed, but it can only be accepted once.
Solution
def solution(operations):
balances = {}
pending = {}
next_transfer_id = 1
result = []
for op in operations:
kind = op[0]
if kind == 'create':
_, acc_id, _ = op
if acc_id in balances:
result.append(False)
else:
balances[acc_id] = 0
result.append(True)
elif kind == 'deposit':
_, acc_id, amount, _ = op
if acc_id not in balances:
result.append(None)
else:
balances[acc_id] += amount
result.append(balances[acc_id])
elif kind == 'transfer':
_, src, dst, amount, _ = op
if src == dst or src not in balances or dst not in balances or balances[src] < amount:
result.append(None)
else:
balances[src] -= amount
pending[next_transfer_id] = (src, dst, amount)
result.append(next_transfer_id)
next_transfer_id += 1
elif kind == 'accept':
_, dst, transfer_id, _ = op
if transfer_id not in pending:
result.append(False)
else:
_, real_dst, amount = pending[transfer_id]
if dst != real_dst or real_dst not in balances:
result.append(False)
else:
balances[real_dst] += amount
del pending[transfer_id]
result.append(True)
elif kind == 'balance':
_, acc_id, _ = op
result.append(balances.get(acc_id))
else:
raise ValueError('Unknown operation type')
return resultTime complexity: O(n) average time. Space complexity: O(a + p), where a is the number of accounts and p is the number of pending transfers.
Hints
- Store current balances separately from pending transfers.
- A transfer should reduce the source balance immediately, even before acceptance.
Part 4: Top Activity at an Exact Timestamp
Constraints
- 1 <= len(records), len(queries) <= 200000
- 0 <= amount <= 10^9
- Records are valid and sorted by non-decreasing timestamp
Examples
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 200, 5), ('pay', 'A', 100, 5), ('deposit', 'B', 250, 5)], [(5, 2)])
Expected Output: [['A', 'B']]
Explanation: At timestamp 5, A has activity 300 and B has activity 250.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('accepted_transfer', 'A', 'B', 40, 3)], [(3, 2)])
Expected Output: [['A', 'B']]
Explanation: A finalized transfer contributes 40 activity to both accounts at timestamp 3.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 50, 4), ('deposit', 'B', 10, 4), ('merge_away', 'A', 4)], [(4, 2), (3, 1)])
Expected Output: [['B'], []]
Explanation: A is merged away at timestamp 4, so it must not appear for timestamp 4 or later.
Input: ([('create', 'A', 1), ('deposit', 'A', 20, 2), ('merge_away', 'A', 3), ('create', 'A', 5), ('deposit', 'A', 7, 5)], [(2, 1), (4, 1), (5, 1)])
Expected Output: [['A'], [], ['A']]
Explanation: The old A is valid at timestamp 2, absent at 4, and the recreated A is valid at 5.
Solution
def solution(records, queries):
from collections import defaultdict
from bisect import bisect_right
activity = defaultdict(lambda: defaultdict(int))
intervals = defaultdict(list)
active_start = {}
for rec in records:
kind = rec[0]
if kind == 'create':
_, acc_id, t = rec
active_start[acc_id] = t
elif kind == 'deposit' or kind == 'pay':
_, acc_id, amount, t = rec
activity[t][acc_id] += amount
elif kind == 'accepted_transfer':
_, src, dst, amount, t = rec
activity[t][src] += amount
activity[t][dst] += amount
elif kind == 'merge_away':
_, acc_id, t = rec
if acc_id in active_start:
intervals[acc_id].append((active_start[acc_id], t))
del active_start[acc_id]
else:
raise ValueError('Unknown record type')
INF = 10 ** 30
for acc_id, start in active_start.items():
intervals[acc_id].append((start, INF))
lookup = {}
for acc_id, ranges in intervals.items():
starts = [start for start, _ in ranges]
ends = [end for _, end in ranges]
lookup[acc_id] = (starts, ends)
def is_active(acc_id, t):
data = lookup.get(acc_id)
if data is None:
return False
starts, ends = data
i = bisect_right(starts, t) - 1
return i >= 0 and t < ends[i]
cache = {}
answer = []
for t, k in queries:
if t not in cache:
ranked = []
for acc_id, amt in activity.get(t, {}).items():
if amt > 0 and is_active(acc_id, t):
ranked.append((-amt, acc_id))
ranked.sort()
cache[t] = [acc_id for _, acc_id in ranked]
answer.append(cache[t][:k])
return answerTime complexity: O(R + sum over distinct queried timestamps of (a_t log a_t + a_t log l)), where a_t is the number of accounts with activity at timestamp t and l is the number of lifetimes for an ID. Space complexity: O(R).
Hints
- Store activity by exact timestamp rather than cumulatively over time.
- Because IDs can be merged away and later recreated, think in terms of active time intervals for each ID.
Part 5: Live Account Merges and Re-Creation
Constraints
- 1 <= len(operations) <= 200000
- 0 <= amount <= 10^9
- Timestamps are integers in non-decreasing order
Examples
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 50, 2), ('deposit', 'B', 20, 3), ('merge', 'A', 'B', 4), ('balance', 'A', 5), ('balance', 'B', 5)],)
Expected Output: [True, True, 50, 20, True, 70, None]
Explanation: B is absorbed into A, so only A remains live with combined balance.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'B', 15, 2), ('merge', 'A', 'B', 3), ('create', 'B', 4), ('balance', 'B', 4), ('deposit', 'B', 5, 5), ('balance', 'A', 6)],)
Expected Output: [True, True, 15, True, True, 0, 5, 15]
Explanation: After being merged away, B can be recreated later as a fresh live account.
Input: ([('create', 'A', 1), ('merge', 'A', 'A', 2), ('merge', 'A', 'B', 3), ('balance', 'A', 4)],)
Expected Output: [True, False, False, 0]
Explanation: You cannot merge an account into itself, and you cannot merge a missing source.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'B', 4, 2), ('merge', 'A', 'B', 3), ('deposit', 'B', 1, 4), ('balance', 'B', 5)],)
Expected Output: [True, True, 4, True, None, None]
Explanation: Once B is merged away, later operations on B are invalid until re-created.
Solution
def solution(operations):
balances = {}
result = []
for op in operations:
kind = op[0]
if kind == 'create':
_, acc_id, _ = op
if acc_id in balances:
result.append(False)
else:
balances[acc_id] = 0
result.append(True)
elif kind == 'deposit':
_, acc_id, amount, _ = op
if acc_id not in balances:
result.append(None)
else:
balances[acc_id] += amount
result.append(balances[acc_id])
elif kind == 'merge':
_, dst, src, _ = op
if dst == src or dst not in balances or src not in balances:
result.append(False)
else:
balances[dst] += balances[src]
del balances[src]
result.append(True)
elif kind == 'balance':
_, acc_id, _ = op
result.append(balances.get(acc_id))
else:
raise ValueError('Unknown operation type')
return resultTime complexity: O(n) average time. Space complexity: O(m), where m is the number of live accounts.
Hints
- For this sub-problem, a merge is just a move of current balance plus deletion of the source ID from the live map.
- Re-creating a merged-away ID is the same as inserting a new key with balance 0.
Part 6: Balance Queries As of a Timestamp
Constraints
- 1 <= len(events), len(queries) <= 200000
- 0 <= amount <= 10^9
- The event log is valid and sorted by non-decreasing timestamp
Examples
Input: ([('create', 'A', 1), ('deposit', 'A', 100, 2), ('pay', 'A', 30, 5)], [('A', 0), ('A', 1), ('A', 4), ('A', 5), ('B', 5)])
Expected Output: [None, 0, 100, 70, None]
Explanation: Queries before creation or for unknown IDs return None.
Input: ([('create', 'A', 1), ('deposit', 'A', 20, 2), ('create', 'B', 3), ('deposit', 'B', 5, 4), ('merge', 'A', 'B', 6)], [('B', 5), ('B', 6), ('A', 6), ('A', 7)])
Expected Output: [5, None, 25, 25]
Explanation: B exists before timestamp 6, but is invalid from timestamp 6 onward after the merge.
Input: ([('create', 'A', 1), ('create', 'B', 1), ('deposit', 'A', 10, 2), ('merge', 'B', 'A', 3), ('create', 'A', 5), ('deposit', 'A', 7, 6)], [('A', 2), ('A', 3), ('A', 5), ('A', 6), ('B', 6)])
Expected Output: [10, None, 0, 7, 10]
Explanation: The old A disappears at timestamp 3, and the recreated A starts fresh at timestamp 5.
Input: ([('create', 'A', 1), ('deposit', 'A', 5, 1), ('pay', 'A', 2, 1)], [('A', 1), ('A', 0)])
Expected Output: [3, None]
Explanation: A query at timestamp 1 sees the state after all timestamp-1 events.
Solution
def solution(events, queries):
from collections import defaultdict
from bisect import bisect_right
INF = 10 ** 30
lifetimes = defaultdict(list)
current = {}
for event in events:
kind = event[0]
if kind == 'create':
_, acc_id, t = event
life = {
'start': t,
'end': INF,
'times': [t],
'balances': [0]
}
lifetimes[acc_id].append(life)
current[acc_id] = life
elif kind == 'deposit':
_, acc_id, amount, t = event
life = current[acc_id]
life['times'].append(t)
life['balances'].append(life['balances'][-1] + amount)
elif kind == 'pay':
_, acc_id, amount, t = event
life = current[acc_id]
life['times'].append(t)
life['balances'].append(life['balances'][-1] - amount)
elif kind == 'merge':
_, dst, src, t = event
dst_life = current[dst]
src_life = current[src]
dst_life['times'].append(t)
dst_life['balances'].append(dst_life['balances'][-1] + src_life['balances'][-1])
src_life['end'] = t
del current[src]
else:
raise ValueError('Unknown event type')
index = {}
for acc_id, lives in lifetimes.items():
starts = [life['start'] for life in lives]
ends = [life['end'] for life in lives]
index[acc_id] = (lives, starts, ends)
answer = []
for acc_id, t in queries:
data = index.get(acc_id)
if data is None:
answer.append(None)
continue
lives, starts, ends = data
i = bisect_right(starts, t) - 1
if i < 0 or t >= ends[i]:
answer.append(None)
continue
life = lives[i]
j = bisect_right(life['times'], t) - 1
answer.append(life['balances'][j])
return answerTime complexity: Preprocessing is O(E). Each query is O(log r + log u), where r is the number of lifetimes for that ID and u is the number of updates in the chosen lifetime. Total: O(E + Q(log r + log u)). Space complexity: O(E).
Hints
- Because an ID can disappear and later come back, model each ID as a sequence of lifetimes.
- Within a lifetime, store balance changes by timestamp so you can answer queries with binary search.