Design an in-memory banking service
Company: Anthropic
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Design an in-memory banking service supporting timestamped operations and edge-case semantics. Implement:
(
1) create_account(id, t): Create a new account at timestamp t. Return false if id currently exists and has not been merged since its last existence. After an account id is merged away, it may be created again at a later timestamp with balance initialized to 0, without erasing pre-merge historical activities.
(
2) deposit(id, amount, t) and pay(id, amount, t): Update balance at timestamp t; amounts are non-negative integers; reject pay if insufficient funds.
(
3) transfer(src, dst, amount, t) to initiate a pending transfer that immediately debits src’s available balance; accept_transfer(dst, transfer_id, t
2) to finalize the transfer into dst at timestamp t2. Activity counting rule: only finalized transfers (at t
2) contribute to activity; initiating a transfer does not.
(
4) top_activity(t, k): At timestamp t, return the k account ids with the largest 'activity' at t, where an account’s activity at t is the sum of absolute amounts of all operations occurring exactly at timestamp t for that account (e.g., deposit 200 and pay 100 at the same t yields activity
300). If an account id was merged before t, it must not appear for t or later; its historical activity prior to the merge remains queryable at earlier timestamps.
(
5) merge_accounts(dst, src, t): Merge src into dst at timestamp t. After merge, queries for src at any timestamp ≥ t (balance, activity, etc.) return None; dst’s balance/history continue. The id src may be re-created later via create_account(src, t'), in which case its balance resets to 0 starting at t', without altering pre-merge history.
(
6) get_balance(id, t): Return the account’s balance as of timestamp t. Specify:
(a) data structures to support these semantics efficiently;
(b) exact return values for invalid operations and the edge cases noted above;
(c) time/space complexity of each operation;
(d) a unit-testing plan that covers the tricky merge and activity rules.
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
Given a chronological list of account operations, return one boolean verdict per operation indicating whether it **succeeds**.
Each account ID is alive only while it is **active**. The two operations toggle this state, and reusing an ID after it has been merged away is allowed.
## Function
```python
def solution(operations):
...
```
## Input
`operations` is a list, in chronological order, of triples:
```
(kind, account_id, timestamp)
```
- **`kind`** — either `'create'` or `'merge_away'`.
- **`account_id`** — a non-empty string (length at most 20).
- **`timestamp`** — an integer. Timestamps are non-decreasing but **do not affect any verdict** and can be ignored.
## Output
Return a **list of booleans** the same length as `operations`, where the i-th entry is the verdict for the i-th operation (in input order).
## Operation rules
Maintain the set of currently **active** IDs (initially empty), then process each operation in order:
- **`create`**: An ID may be created only if it is **not** currently active.
- If the ID is **not** active → it becomes active and the operation **succeeds** (`True`).
- If the ID **is** already active → the operation **fails** (`False`) and the active set is unchanged.
- **`merge_away`**: An ID is merged into some other account elsewhere and stops existing. This may only be applied to an ID that is currently active.
- If the ID **is** active → it becomes inactive and the operation **succeeds** (`True`).
- If the ID is **not** active → the operation **fails** (`False`) and the active set is unchanged.
Because a successful `merge_away` removes the ID from the active set, that **same ID may be created again later** as a fresh account, and the new `create` succeeds.
You only need to decide success or failure of each operation; you are not asked to track balances or historical activity.
## Examples
```
operations = [('create', 'A', 1), ('create', 'A', 2)]
-> [True, False] # second create fails: 'A' is already active
operations = [('create', 'A', 1), ('merge_away', 'A', 5), ('create', 'A', 7)]
-> [True, True, True] # 'A' is reusable after being merged away
operations = [('merge_away', 'X', 1), ('create', 'X', 2)]
-> [False, True] # nothing active to merge away, then create succeeds
```
## Constraints
- `1 <= len(operations) <= 200000`
- Account IDs are non-empty strings of length at most 20.
- Timestamps are integers in non-decreasing order.
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.
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
Simulate the balance updates of a simple in-memory bank, processing a stream of timestamped operations and returning the result of each one.
Implement:
```python
def solution(operations):
...
```
## Input
`operations` is a list of operations given in chronological order. Each operation is a **tuple** whose first element is a string tag identifying its type:
- **Create:** `('create', acc_id, timestamp)` — request to open a new account `acc_id`.
- **Deposit:** `('deposit', acc_id, amount, timestamp)` — add `amount` to account `acc_id`.
- **Pay:** `('pay', acc_id, amount, timestamp)` — withdraw `amount` from account `acc_id`.
`amount` is a non-negative integer (zero is allowed). `timestamp` is an integer; timestamps are non-decreasing. Because the operations already arrive in chronological order, the timestamp is informational only — you do **not** need to read or sort by it.
## Output
Return a **list** with exactly one entry per operation, in the same order as the input. The entry depends on the operation type:
- **`create`** → `True` if the account was newly opened, or `False` if an account with that `acc_id` already exists (the existing account is left unchanged).
- **`deposit`** → `None` if the account does not exist (the deposit is rejected). Otherwise, add `amount` to the balance and return the **new balance** (an integer). A zero-amount deposit on an existing account is valid and returns the unchanged balance.
- **`pay`** → `None` if the account does not exist **or** if the balance is less than `amount` (insufficient funds — the payment is rejected and the balance is unchanged). Otherwise, subtract `amount` from the balance and return the **new balance** (an integer). A payment that brings the balance to exactly `0` is allowed; a zero-amount payment always succeeds when the account exists.
## Rules
- All balances start at `0` when an account is created.
- A payment is allowed only when `amount <= balance` (so an account may be drained to exactly `0`, but not overdrawn).
- A deposit or payment on a non-existent account is rejected with `None` and does not create the account.
## Example
For the input
```python
[('create', 'A', 1), ('deposit', 'A', 100, 2), ('pay', 'A', 30, 3)]
```
the result is `[True, 100, 70]`: account `A` is opened (`True`), the deposit brings the balance to `100`, and the payment of `30` leaves `70`.
## Constraints
- `1 <= len(operations) <= 200000`
- `0 <= amount <= 10^9`
- Timestamps are integers in non-decreasing order.
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.
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
Implement a small **banking engine** that supports **pending (escrow-style) transfers**: a transfer debits the source account immediately, but the destination is not credited until the transfer is explicitly **accepted**.
## Function
```python
def solution(operations):
...
```
`operations` is a list of operation tuples. Process them **in order**, maintaining account balances and a ledger of outstanding pending transfers. For each operation, append one result to an output list, and **return that list**.
Every operation tuple ends with an integer `timestamp` (timestamps are non-decreasing and do not affect the logic). The first element of each tuple is the operation **kind**.
## Operations
**`('create', acc_id, timestamp)`** — Create a new account with balance `0`.
- If `acc_id` already exists, change nothing and append `False`.
- Otherwise create it at balance `0` and append `True`.
**`('deposit', acc_id, amount, timestamp)`** — Add `amount` to an account.
- If `acc_id` does not exist, change nothing and append `None`.
- Otherwise add `amount` to its balance and append the **new balance**.
**`('transfer', src, dst, amount, timestamp)`** — Start a pending transfer from `src` to `dst`.
- The transfer is **rejected** (change nothing, append `None`) if any of these hold:
- `src == dst`, or
- `src` does not exist, or
- `dst` does not exist, or
- `balances[src] < amount` (insufficient funds).
- Otherwise it **succeeds**: immediately **debit** `amount` from `src`, create a pending transfer recording `(src, dst, amount)`, and append the transfer's **id**.
- Transfer ids are unique integers assigned in order, **starting at 1** and incrementing by 1 for each successful transfer.
- The destination is **not** credited at this point — the money sits in escrow until accepted.
**`('accept', dst, transfer_id, timestamp)`** — Complete a pending transfer.
- Append `False` (and change nothing) if any of these hold:
- `transfer_id` is not an outstanding pending transfer, or
- the supplied `dst` does **not** match the transfer's recorded destination, or
- that destination account no longer exists.
- Otherwise **credit** the recorded `amount` to the destination account, remove the pending transfer, and append `True`.
**`('balance', acc_id, timestamp)`** — Look up an account's balance.
- Append the current balance of `acc_id`, or `None` if the account does not exist.
## Important rules
- **A failed `accept` must not destroy the pending transfer.** If `accept` is called with the wrong destination (or otherwise fails the checks above), it returns `False` but leaves the pending transfer intact, so a later **correct** `accept` can still complete it.
- An `amount` of `0` is valid for both `deposit` and `transfer` (a 0-amount transfer with both accounts present succeeds and produces a normal transfer id).
## Examples
For `operations = [('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)]` the result is `[True, True, 100, 1, 60, True, 40]`.
For `operations = [('create','A',1), ('create','B',1), ('deposit','A',20,2), ('transfer','A','B',30,3), ('balance','A',4), ('accept','B',1,5)]` the result is `[True, True, 20, None, 20, False]` — the transfer is rejected for insufficient funds (returns `None`), `A` keeps its `20`, and the later `accept` of the non-existent transfer `1` returns `False`.
## Constraints
- `1 <= len(operations) <= 200000`
- `0 <= amount <= 10^9`
- Timestamps are integers in non-decreasing order.
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.
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
Given a log of completed banking **records** and a list of independent **queries**, report, for each query, the accounts with the highest *activity* at an exact timestamp.
## Activity at a timestamp
For a timestamp `t`, an account's **activity at `t`** is the sum of the amounts of all operations that occur at *exactly* `t` for that account. (Amounts are non-negative, so this is simply their sum.)
## Inputs
### `records`
A list of operation tuples, **sorted by non-decreasing timestamp**. Each tuple's first element is a kind string. The recognized kinds and how each contributes to activity are:
- **`('create', acc_id, t)`** — Begins a new lifetime for `acc_id` at time `t`. Adds **no** activity.
- **`('deposit', acc_id, amount, t)`** — Adds `amount` to `acc_id`'s activity at `t`.
- **`('pay', acc_id, amount, t)`** — Adds `amount` to `acc_id`'s activity at `t`.
- **`('accepted_transfer', src, dst, amount, t)`** — Adds `amount` to **both** `src` and `dst`'s activity at `t`.
- **`('merge_away', acc_id, t)`** — Makes `acc_id` **ineligible** to appear in any query result for timestamp `t` **or any later timestamp**. Adds no activity.
### `queries`
A list of `(t, k)` pairs. Each query is independent.
## Account lifetimes and eligibility
An account becomes **active** when it is `create`d and remains active until it is `merge_away`'d:
- A `merge_away(acc_id, t)` ends the current lifetime **at `t`, inclusive** — the account is *not* eligible at `t` itself, nor at any later time, for that lifetime.
- Therefore an account created at time `s` and merged away at time `e` is eligible only for timestamps in the half-open range `s <= t < e`.
- An account that is never merged away stays active for all timestamps at or after its `create` time.
- A later `create` of the same ID starts a **fresh, independent lifetime**. An ID can thus be active in one range, ineligible in a gap, and active again in a later range.
## Output
Return a list with **one entry per query**, in the same order as `queries`.
For a query `(t, k)`, produce the list of account IDs that satisfy **all** of:
1. The account is **active** (eligible) at timestamp `t`, and
2. The account has **positive** activity at exactly `t` (activity `> 0`).
Order these IDs by:
- **descending activity** at `t`, then
- **ascending lexicographic order of the ID** to break ties.
Return at most the first **`k`** IDs from this ordering. If no account qualifies (including when no operations occur at `t`), return an empty list for that query.
## Constraints
- `1 <= len(records), len(queries) <= 200000`
- `0 <= amount <= 10^9`
- Records are valid and sorted by non-decreasing timestamp.
## Examples
- `deposit A 200` and `pay A 100` at `t=5`, plus `deposit B 250` at `t=5` (both accounts active), query `(5, 2)` → `[['A', 'B']]` (A has activity 300, B has 250).
- `accepted_transfer A B 40` at `t=3` (both active), query `(3, 2)` → `[['A', 'B']]` (both have activity 40; tie broken lexicographically).
- A and B both deposit at `t=4`, then `merge_away A` at `t=4`; queries `(4, 2), (3, 1)` → `[['B'], []]` (A is ineligible from `t=4` on; nothing happens at `t=3`).
- A active at `t=2`, merged away at `t=3`, re-created at `t=5` and deposits there; queries `(2, 1), (4, 1), (5, 1)` → `[['A'], [], ['A']]` (active in `[1,3)`, ineligible at `t=4`, active again from `t=5`).
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.
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
Process a live stream of banking operations on accounts, where accounts can be **created**, **deposited into**, **merged**, and **queried** — returning one result per operation in order.
Implement:
```python
def solution(operations):
```
## Input
`operations` is a list of operation tuples. Each tuple's first element is a string naming the operation type; the remaining elements depend on the type. Every operation ends with an integer timestamp `t`. Timestamps are non-decreasing but do not affect the result of any operation.
Account IDs are arbitrary hashable values (e.g. strings). An account is **live** from the moment it is created until it is removed by a merge; it is not live before its first creation or after being merged away.
## Operations
Process the operations strictly in order. Each produces exactly one entry appended to the result list:
- **`('create', acc_id, t)`** — Create a new account with balance `0`.
- If `acc_id` is **already live**, do nothing and append `False`.
- Otherwise create it (starting at balance `0`) and append `True`. A re-created account always starts fresh at `0`.
- **`('deposit', acc_id, amount, t)`** — Add `amount` to an account's balance.
- If `acc_id` is **not live**, do nothing and append `None`.
- Otherwise add `amount` to its balance and append the **new** balance.
- **`('merge', dst, src, t)`** — Move the full current balance of `src` into `dst`, then remove `src` from the live system immediately.
- If `dst == src`, or `dst` is not live, or `src` is not live, do nothing and append `False`.
- Otherwise add `src`'s balance to `dst`'s balance, remove `src` (it becomes non-live immediately, so subsequent deposits/queries on `src` return `None` until it is created again), and append `True`.
- **`('balance', acc_id, t)`** — Query an account's current balance.
- Append the account's current balance if it is **live**, otherwise append `None`.
## Output
Return a list with one element per operation, in the same order as the input, using the per-operation result values described above.
## Notes
- This problem only concerns the **live state** observed while processing operations in order. Pre-merge history is conceptually preserved but is never queried here.
- After a merge, `src` is gone immediately; a later `create` for that same ID starts a brand-new account at balance `0`.
## Constraints
- `1 <= len(operations) <= 200000`
- `0 <= amount <= 10^9`
- Timestamps are integers in non-decreasing order.
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]
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]
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
Implement `solution(events, queries)` to answer **point-in-time balance queries** against an account event log.
You are given a chronological log of account `events` and a list of independent `queries`. For each query `(id, t)`, return the balance of account `id` **as of timestamp `t`** — the state after every event whose timestamp is `<= t` has been applied. Queries do not modify state and are answered independently.
## Input
- **`events`** — a list of event tuples, already sorted by **non-decreasing timestamp**. Each event is one of:
- `('create', id, t)` — opens a new account `id` (or a fresh lifetime for a reused `id`) with balance `0` at time `t`.
- `('deposit', id, amount, t)` — adds `amount` to `id`'s balance at time `t`.
- `('pay', id, amount, t)` — subtracts `amount` from `id`'s balance at time `t`.
- `('merge', dst, src, t)` — folds account `src` into account `dst` at time `t`.
- **`queries`** — a list of `(id, t)` tuples to answer.
## Output
Return a **list** with one entry per query, in the same order:
- the **integer balance** of `id` as of `t`, or
- `None` if `id` is not valid at `t` (see rules below).
## Rules
- **Same-timestamp events are all applied first.** A query at time `t` reflects every event with timestamp `<= t`, including events that share the timestamp `t`. (E.g. a deposit and a pay both at `t=1` are both included for a query at `t=1`.)
- **Before creation → `None`.** If `t` is earlier than the account's `create` time (or the `id` never appears), return `None`.
- **Merge semantics.** After `merge(dst, src, t)`:
- `dst`'s balance at time `t` becomes `dst_balance + src_balance` (each taken just before the merge).
- `src` becomes **invalid for every timestamp `>= t`**; a query on `src` at any `t' >= t` returns `None`. Queries on `src` for timestamps **before** `t` still return its historical balance.
- **Reused IDs start a brand-new lifetime.** If an `id` that was previously merged away is `create`d again later, it begins a fresh account with balance `0`. This new lifetime does **not** affect answers for any earlier timestamp — queries before the re-creation still resolve against the original lifetime.
## Constraints
- `1 <= len(events), len(queries) <= 200000`
- `0 <= amount <= 10^9`
- The event log is valid and sorted by non-decreasing 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.
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.