In-Memory Bank System With Top-N Activity Ranking and Expiring Pending Transfers
CircleSoftware Engineer · Technical ScreenLast updated
Implement an in-memory banking class in stages: create accounts, deposit and pay, rank the top n accounts by transaction activity, and support transfers that hold funds until the recipient accepts within a time window or they are refunded. Tests state modeling, validation, and timestamp-driven expiry.
Implement an in-memory banking system as a single class. The task is delivered in stages: each stage adds behavior to the same class, and everything from earlier stages must keep working unchanged.
Every operation receives an integer timestamp as its first argument. Assume a class shaped like this (names are illustrative; keep the semantics):
class Bank:
def __init__(self, transfer_window: int): ...
def create_account(self, timestamp: int, account_id: str) -> bool: ...
def deposit(self, timestamp: int, account_id: str, amount: int) -> int | None: ...
def pay(self, timestamp: int, account_id: str, amount: int) -> int | None: ...
def top_activity(self, timestamp: int, n: int) -> list[tuple[str, int]]: ...
def transfer(self, timestamp: int, source_id: str, target_id: str, amount: int) -> str | None: ...
def accept_transfer(self, timestamp: int, account_id: str, transfer_id: str) -> bool: ...
Constraints and Clarifications
- Timestamps are integers and strictly increase from one call to the next.
- Amounts are positive integers; a balance may never go negative.
- Account ids are non-empty strings.
transfer_windowis a positive integer fixed when the bank is created; the length of the acceptance window was not specified, so treat it as a parameter.- A failed operation changes no state.
Clarifying Questions
- Should invalid calls (unknown account, insufficient funds) return a failure value or raise? This version returns
NoneorFalse. - Can an account be deleted or renamed? (Assume not.)
- Are timestamps guaranteed strictly increasing, or must the class tolerate out-of-order calls? (Assume strictly increasing.)
Part 1 — Accounts, deposits and payments
Implement:
create_account: creates an account with balance 0; returnsFalseif the id already exists.deposit: addsamountto the account and returns the new balance; returnsNoneif the account does not exist.pay: withdrawsamountand returns the new balance; returnsNoneif the account does not exist or its balance is smaller thanamount.
What This Part Should Cover
- An account registry keyed by id, with duplicate-creation handling
- Rejection of unknown accounts and overdrafts without partial state changes
- Return values that later stages can build on
Part 2 — Activity statistics and top n
Every successful deposit and pay counts toward its account's activity. Implement top_activity(timestamp, n), which returns up to n accounts with the highest activity, highest first, as (account_id, activity) pairs.
Clarifying Questions for this Part
- Is activity the total amount deposited plus paid, the amount paid only, or the number of transactions?
- How are accounts with equal activity ordered?
- What should be returned when
nexceeds the number of accounts? - Do rejected deposits or payments count?
What This Part Should Cover
- Incremental aggregation versus recomputation at query time
- A deterministic order with an explicit tie-break
- Cost of updates versus queries, and which structure fits which query rate
Part 3 — Transfers with a hold, acceptance, and expiry
Implement transfers between two accounts:
transfer(timestamp, source_id, target_id, amount)immediately deductsamountfrom the source account (the money is held) but does not credit the target. It returns a new unique transfer id, orNoneif either account does not exist, the two ids are the same, or the source balance is smaller thanamount.accept_transfer(timestamp, account_id, transfer_id)credits the target account only if the transfer exists, is still pending,account_idis its target, and acceptance happens withintransfer_windowof the moment the transfer was created. It returnsTrueon success andFalseotherwise.- A pending transfer that is not accepted within the window expires, and the held amount returns to the source account.
Clarifying Questions for this Part
- Is an acceptance at exactly
created_at + transfer_windowstill valid? - Can the sender cancel a pending transfer?
- Should transfers, or refunds of expired transfers, count toward the activity statistic from Part 2?
What This Part Should Cover
- Hold semantics: held money is unavailable to
payand to other transfers, and is never credited twice - When and in what order expirations are applied, and that each refund happens exactly once
- Validation of acceptance: unknown id, wrong account, already accepted, already expired
- Data structures for pending transfers and their per-operation cost
Hints
Hint 1
Validate before mutating: Settle every failure condition of `pay` before touching any balance, so a rejected payment leaves no trace that a later stage could trip over.
Hint 2
Where the numbers live: Ask whether this query should replay every past transaction or read something that `deposit` and `pay` already keep up to date.
Hint 3
Give the transfer a lifecycle: Write down the states a transfer can be in and which calls may move it from one state to another; every legal transition should happen exactly once.
Hint 4
Nobody calls you at the deadline: No method is invoked at the exact moment a transfer expires. Decide which calls must first account for everything that expired since the previous call.
Follow-up questions
- Let the sender cancel a pending transfer. What new state does that add, and how do cancellation and expiry interact?
- With millions of pending transfers, how do you keep expiry handling from making an unrelated `deposit` slow?
- If `top_activity` had to answer "as of" an earlier timestamp, what would you need to store?
- If calls could arrive out of timestamp order, which parts of your design break first?
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.