Banking System Simulation
Company: Anthropic
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
# Banking System Simulation
You are building an in-memory banking system that is driven by a time-ordered stream of operations. Implement the system so that it processes a list of queries and returns one result per query.
Each query is a list of strings. The **first element is the operation name**; the remaining elements are its string arguments. The **second element of every query is a `timestamp`** (an integer number of milliseconds, given in strictly increasing order across the whole input). Amounts are non-negative integers. Account identifiers are arbitrary non-empty strings.
Your function receives `queries: List[List[str]]` and must return `List[str]` — the result of each query as a string (use an empty string `""` when an operation is invalid, as defined below). Numbers in the output are rendered without a currency symbol or thousands separators (e.g. `"1500"`).
The system is leveled: implement the levels in order. Each higher level adds operations; earlier operations keep working unchanged.
## Level 1 — Accounts and balances
- `["CREATE_ACCOUNT", timestamp, account_id]` → `"true"` if a new account is created; `"false"` if an account with that id already exists.
- `["DEPOSIT", timestamp, account_id, amount]` → the account's new balance as a string. If the account does not exist, return `""`.
- `["GET_BALANCE", timestamp, account_id]` → the account's current balance. If the account does not exist (or no longer exists), return `""`.
## Level 2 — Transfers and spending ranks
- `["TRANSFER", timestamp, source_id, target_id, amount]` → the source account's resulting balance. Return `""` if either account is missing, if `source_id == target_id`, or if the source has insufficient funds (no transfer happens in that case).
- `["TOP_SPENDERS", timestamp, n]` → the top `n` accounts ranked by **total outgoing amount** (the sum of all `TRANSFER` amounts they sent plus all `PAY` amounts, see Level 3), as a comma-and-space separated string: `"id1(total1), id2(total2)"`. Rank highest total first; break ties by account id in ascending lexicographic order. Include every existing account; if fewer than `n` accounts exist, return all of them.
## Level 3 — Payments with deferred cashback
- `["PAY", timestamp, account_id, amount]` → debits `amount` immediately and returns a new payment id of the form `"payment1"`, `"payment2"`, … (a single global counter, incremented on every successful `PAY`). Return `""` if the account is missing or has insufficient funds. Each payment earns **2% cashback** (`floor(amount * 2 / 100)`) that is credited back to the same account **exactly `86400000` ms (24 hours) after** the payment timestamp.
- `["GET_PAYMENT_STATUS", timestamp, account_id, payment]` → `"IN_PROGRESS"` if the cashback has not yet been credited at `timestamp`, `"CASHBACK_RECEIVED"` if it has. Return `""` if the account does not exist or the payment id does not belong to that account.
Cashback that is due must be settled (added to the balance) **before** evaluating any operation whose timestamp is at or after the cashback time. In other words, every operation should first apply all pending cashbacks whose credit time is `<= timestamp`.
## Level 4 — Account merge
- `["MERGE_ACCOUNTS", timestamp, account_id_1, account_id_2]` → `"true"` on success, `"false"` if either account is missing or `account_id_1 == account_id_2`. Merging folds `account_id_2` into `account_id_1`: balances are summed, outgoing-total histories are summed, and any of account 2's pending cashbacks are redirected to account 1. After the merge, `account_id_2` no longer exists, but a `GET_PAYMENT_STATUS` query for a payment originally made by account 2 must still succeed when issued against `account_id_1`.
## Constraints
- `1 <= len(queries) <= 10^4`.
- Timestamps are strictly increasing integers in `[1, 10^15]`.
- Amounts are integers in `[0, 10^9]`.
- All outputs are strings.
## Example
Input:
```
[
["CREATE_ACCOUNT", "1", "alice"],
["CREATE_ACCOUNT", "2", "bob"],
["DEPOSIT", "3", "alice", "2000"],
["TRANSFER", "4", "alice", "bob", "500"],
["TOP_SPENDERS", "5", "2"],
["PAY", "6", "alice", "1000"],
["GET_PAYMENT_STATUS", "7", "alice", "payment1"],
["GET_PAYMENT_STATUS", "86400007", "alice", "payment1"],
["GET_BALANCE", "86400008", "alice"]
]
```
Output:
```
["true", "true", "2000", "1500", "alice(500), bob(0)", "payment1", "IN_PROGRESS", "CASHBACK_RECEIVED", "520"]
```
Explanation of the last two lines: alice paid 1000 at t=6, leaving 500. At t=86400006 (24h later) the 2% cashback of 20 is credited, so by t=86400008 her balance is `500 + 20 = 520`.
Overview: This question evaluates a candidate's ability to design a stateful, multi-level simulation system that processes a time-ordered stream of operations. It tests object-oriented design, incremental complexity management, and handling of deferred effects like scheduled cashback credits. Such object-modeling questions are commonly used in coding interviews to assess practical application of data structures under evolving requirements.
Banking System Simulation - Level 1: Accounts & Balances
You are building an in-memory banking system driven by a time-ordered stream of operations. Process `queries: List[List[str]]` and return `List[str]` — one result per query. The first element of each query is the operation name and the second is an integer `timestamp` (ms, strictly increasing). Numbers are rendered as plain strings (no currency symbol, no separators).
## Level 1 — Accounts and balances
- `["CREATE_ACCOUNT", timestamp, account_id]` → `"true"` if a new account is created; `"false"` if that id already exists.
- `["DEPOSIT", timestamp, account_id, amount]` → the account's new balance. Return `""` if the account does not exist.
- `["GET_BALANCE", timestamp, account_id]` → the account's current balance, or `""` if it does not exist.
Constraints
- 1 <= len(queries) <= 10^4.
- Every query is a list of strings; query[0] is the operation name and query[1] is an integer timestamp (ms).
- Timestamps are strictly increasing integers in [1, 10^15].
- Amounts are integers in [0, 10^9]. Account ids are arbitrary non-empty strings.
- Return List[str]: one result string per query; use "" for an invalid operation.
Examples
Input: [['CREATE_ACCOUNT', '1', 'alice'], ['CREATE_ACCOUNT', '2', 'bob'], ['CREATE_ACCOUNT', '3', 'alice'], ['DEPOSIT', '4', 'alice', '2000'], ['DEPOSIT', '5', 'bob', '100'], ['DEPOSIT', '6', 'ghost', '50'], ['GET_BALANCE', '7', 'alice'], ['GET_BALANCE', '8', 'ghost']]
Expected Output: ['true', 'true', 'false', '2000', '100', '', '2000', '']
Explanation: Duplicate CREATE returns false; DEPOSIT/GET_BALANCE on a missing account return ''.
Input: [['CREATE_ACCOUNT', '1', 'x'], ['DEPOSIT', '2', 'x', '0'], ['DEPOSIT', '3', 'x', '500'], ['DEPOSIT', '4', 'x', '500'], ['GET_BALANCE', '5', 'x']]
Expected Output: ['true', '0', '500', '1000', '1000']
Explanation: Deposits accumulate; a zero deposit is valid and returns the running balance.
Hints
- A single dict mapping account_id -> balance is enough for Level 1.
- CREATE_ACCOUNT must not overwrite an existing account: check membership first and return "false" if present.
- DEPOSIT and GET_BALANCE on a missing account both return the empty string, not "0".
Banking System Simulation - Level 2: Transfers & Top Spenders
Extends Level 1 (CREATE_ACCOUNT / DEPOSIT / GET_BALANCE keep working unchanged). Track each account's total **outgoing** amount — the sum of every TRANSFER it sends (plus every PAY at Level 3).
## Level 2 — Transfers and spending ranks
- `["TRANSFER", timestamp, source_id, target_id, amount]` → the source's resulting balance. Return `""` (and make no change) if either account is missing, if `source_id == target_id`, or if the source has insufficient funds.
- `["TOP_SPENDERS", timestamp, n]` → the top `n` accounts by total outgoing amount as `"id1(total1), id2(total2)"`. Rank by highest total first, breaking ties by account id in ascending lexicographic order. Include every existing account (even those with 0 outgoing); if fewer than `n` exist, return all of them.
Constraints
- 1 <= len(queries) <= 10^4.
- Every query is a list of strings; query[0] is the operation name and query[1] is an integer timestamp (ms).
- Timestamps are strictly increasing integers in [1, 10^15].
- Amounts are integers in [0, 10^9]. Account ids are arbitrary non-empty strings.
- Return List[str]: one result string per query; use "" for an invalid operation.
Examples
Input: [['CREATE_ACCOUNT', '1', 'alice'], ['CREATE_ACCOUNT', '2', 'bob'], ['DEPOSIT', '3', 'alice', '2000'], ['TRANSFER', '4', 'alice', 'bob', '500'], ['TOP_SPENDERS', '5', '2'], ['GET_BALANCE', '6', 'bob']]
Expected Output: ['true', 'true', '2000', '1500', 'alice(500), bob(0)', '500']
Explanation: A transfer returns the source's new balance; TOP_SPENDERS lists every account incl. bob(0).
Input: [['CREATE_ACCOUNT', '1', 'a'], ['CREATE_ACCOUNT', '2', 'b'], ['DEPOSIT', '3', 'a', '100'], ['TRANSFER', '4', 'a', 'a', '10'], ['TRANSFER', '5', 'a', 'ghost', '10'], ['TRANSFER', '6', 'ghost', 'a', '10'], ['TRANSFER', '7', 'a', 'b', '500'], ['TRANSFER', '8', 'a', 'b', '100'], ['GET_BALANCE', '9', 'a'], ['GET_BALANCE', '10', 'b'], ['TOP_SPENDERS', '11', '5']]
Expected Output: ['true', 'true', '100', '', '', '', '', '0', '0', '100', 'a(100), b(0)']
Explanation: Transfer is invalid (returns '') on same id, missing party, or insufficient funds; a valid one debits the source.
Hints
- Store both a balance and a running outgoing total per account; a transfer adds `amount` to the source's outgoing total.
- Validate a transfer fully before mutating: missing account, self-transfer, and insufficient funds must all leave state untouched.
- For TOP_SPENDERS sort with key (-outgoing, account_id) so higher totals come first and ties break by ascending id.
Banking System Simulation - Level 3: Payments & Deferred Cashback
Extends Levels 1–2. Adds payments that earn cashback credited 24h later. Before evaluating any query, first apply all pending cashbacks whose credit time is `<= timestamp`.
## Level 3 — Payments with deferred cashback
- `["PAY", timestamp, account_id, amount]` → debits `amount` immediately and returns a new payment id `"payment1"`, `"payment2"`, … from a single global counter (incremented on every successful PAY). Return `""` if the account is missing or has insufficient funds. A PAY also counts toward the account's outgoing total. Each payment earns **2% cashback** = `floor(amount * 2 / 100)`, credited back to the same account exactly `86400000` ms after the payment.
- `["GET_PAYMENT_STATUS", timestamp, account_id, payment]` → `"IN_PROGRESS"` if the cashback is not yet credited at `timestamp`, else `"CASHBACK_RECEIVED"`. Return `""` if the account does not exist or the payment id does not belong to it.
Constraints
- 1 <= len(queries) <= 10^4.
- Every query is a list of strings; query[0] is the operation name and query[1] is an integer timestamp (ms).
- Timestamps are strictly increasing integers in [1, 10^15].
- Amounts are integers in [0, 10^9]. Account ids are arbitrary non-empty strings.
- Return List[str]: one result string per query; use "" for an invalid operation.
Examples
Input: [['CREATE_ACCOUNT', '1', 'alice'], ['CREATE_ACCOUNT', '2', 'bob'], ['DEPOSIT', '3', 'alice', '2000'], ['TRANSFER', '4', 'alice', 'bob', '500'], ['TOP_SPENDERS', '5', '2'], ['PAY', '6', 'alice', '1000'], ['GET_PAYMENT_STATUS', '7', 'alice', 'payment1'], ['GET_PAYMENT_STATUS', '86400007', 'alice', 'payment1'], ['GET_BALANCE', '86400008', 'alice']]
Expected Output: ['true', 'true', '2000', '1500', 'alice(500), bob(0)', 'payment1', 'IN_PROGRESS', 'CASHBACK_RECEIVED', '520']
Explanation: The spec example: 2% cashback (20) on the 1000 payment credits 24h later, lifting alice's balance to 520.
Input: [['CREATE_ACCOUNT', '1', 'a'], ['CREATE_ACCOUNT', '2', 'b'], ['DEPOSIT', '3', 'a', '1000'], ['PAY', '4', 'a', '2000'], ['PAY', '5', 'ghost', '10'], ['PAY', '6', 'a', '1000'], ['GET_PAYMENT_STATUS', '7', 'a', 'payment1'], ['GET_PAYMENT_STATUS', '8', 'b', 'payment1'], ['GET_PAYMENT_STATUS', '9', 'a', 'payment99'], ['GET_PAYMENT_STATUS', '10', 'ghost', 'payment1'], ['TOP_SPENDERS', '11', '2']]
Expected Output: ['true', 'true', '1000', '', '', 'payment1', 'IN_PROGRESS', '', '', '', 'a(1000), b(0)']
Explanation: PAY fails ('') on insufficient funds or missing account; GET_PAYMENT_STATUS is '' for a foreign/unknown payment or missing account; the payment counts toward outgoing.
Hints
- Keep a min-heap of pending cashbacks keyed by credit time (payment_timestamp + 86400000); at the start of every query pop and credit all entries with credit_time <= timestamp.
- Because timestamps are strictly increasing, a single sweep of the heap per query settles everything that is due.
- GET_PAYMENT_STATUS is a pure time comparison: CASHBACK_RECEIVED iff timestamp >= that payment's credit time; also verify the payment id actually belongs to the queried account.
Banking System Simulation - Level 4: Account Merge
Extends Levels 1–3 with account merging.
## Level 4 — Account merge
- `["MERGE_ACCOUNTS", timestamp, account_id_1, account_id_2]` → `"true"` on success; `"false"` if either account is missing or `account_id_1 == account_id_2`. Merging folds `account_id_2` into `account_id_1`: balances are summed, outgoing-total histories are summed, and any of account 2's pending cashbacks are redirected to account 1. After the merge `account_id_2` no longer exists, but a `GET_PAYMENT_STATUS` for a payment originally made by account 2 must still succeed when issued against `account_id_1`.
All earlier operations (CREATE_ACCOUNT, DEPOSIT, GET_BALANCE, TRANSFER, TOP_SPENDERS, PAY, GET_PAYMENT_STATUS) keep working unchanged.
Constraints
- 1 <= len(queries) <= 10^4.
- Every query is a list of strings; query[0] is the operation name and query[1] is an integer timestamp (ms).
- Timestamps are strictly increasing integers in [1, 10^15].
- Amounts are integers in [0, 10^9]. Account ids are arbitrary non-empty strings.
- Return List[str]: one result string per query; use "" for an invalid operation.
Examples
Input: [['CREATE_ACCOUNT', '1', 'alice'], ['CREATE_ACCOUNT', '2', 'bob'], ['DEPOSIT', '3', 'alice', '2000'], ['TRANSFER', '4', 'alice', 'bob', '500'], ['TOP_SPENDERS', '5', '2'], ['PAY', '6', 'alice', '1000'], ['GET_PAYMENT_STATUS', '7', 'alice', 'payment1'], ['GET_PAYMENT_STATUS', '86400007', 'alice', 'payment1'], ['GET_BALANCE', '86400008', 'alice']]
Expected Output: ['true', 'true', '2000', '1500', 'alice(500), bob(0)', 'payment1', 'IN_PROGRESS', 'CASHBACK_RECEIVED', '520']
Explanation: Levels 1-3 keep working unchanged when Level 4 is implemented (the spec example).
Input: [['CREATE_ACCOUNT', '1', 'a'], ['CREATE_ACCOUNT', '2', 'b'], ['DEPOSIT', '3', 'a', '100'], ['DEPOSIT', '4', 'b', '200'], ['MERGE_ACCOUNTS', '5', 'a', 'b'], ['GET_BALANCE', '6', 'a'], ['GET_BALANCE', '7', 'b'], ['MERGE_ACCOUNTS', '8', 'a', 'a'], ['MERGE_ACCOUNTS', '9', 'a', 'ghost'], ['MERGE_ACCOUNTS', '10', 'ghost', 'a']]
Expected Output: ['true', 'true', '100', '200', 'true', '300', '', 'false', 'false', 'false']
Explanation: Merge folds b into a (balances summed) and removes b; merge is 'false' on same id or a missing account.
Hints
- Track payment ownership in a separate `payment_id -> account_id` map and keep a set of payment ids on each account; a merge rewrites those entries from account 2 to account 1.
- Key the pending-cashback heap by payment id (not account id) and resolve the current owner at settle time — then a merge that redirects ownership automatically redirects the cashback.
- Merge is invalid ("false") on a missing account or a self-merge; on success sum balances and outgoing totals, then delete account 2 so later lookups on it return "".
Community answers
Answer by andrew.rosses12
from typing import List
from itertools import islice, count
import math
IN_PROGRESS = 0
CASHBACK_RECEIVED = 1
class Bank:
def init(self):
self.accounts = dict()
self.outgoing_transfers = dict()
self.payment_counter = count(1)
self.cashback = dict()
self.cashback_status = dict()
def create_account(self, timestamp, account_id):
if account_id in self.accounts:
return 'false'
self.accounts[account_id] = 0
self.outgoing_transfers[account_id] = 0
return 'true'
def deposit(self, timestamp, account_id, amount):
if account_id in self.accounts:
self.accounts[account_id] += int(amount)
return str(self.accounts[account_id])
return ''
def get_balance(self, timestamp, account_id):
return str(self.accounts[account_id]) if account_id in self.accounts else ''
def transfer(self, timestamp, source_id, target_id, amount):
if source_id == target_id:
return ''
if source_id not in self.accounts or target_id not in self.accounts:
return ''
if int(amount) > self.accounts[source_id]:
return ''
amount = int(amount)
self.accounts[source_id] -= amount
self.accounts[target_id] += amount
self.outgoing_transfers[source_id] += amount
return str(self.accounts[source_id])
def top_spenders(self, timestamp, n):
res = sorted(self.outgoing_transfers.items(), key=lambda x: (-x[1], x[0]))
return ', '.join([f'{id}({val})' for id, val in islice(res, int(n))])
def pay(self, timestamp, account_id, amount):
amount = int(amount)
if account_id not in self.accounts or self.accounts[account_id] < amount:
return ''
self.outgo