In-Memory Bank System With Top-N Activity Ranking and Expiring Pending Transfers

Quick Overview

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.

In-Memory Bank System With Top-N Activity Ranking and Expiring Pending Transfers

Company: Circle

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Technical Screen

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): ```python 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_window` is 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 `None` or `False`. - 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; returns `False` if the id already exists. - `deposit`: adds `amount` to the account and returns the new balance; returns `None` if the account does not exist. - `pay`: withdraws `amount` and returns the new balance; returns `None` if the account does not exist or its balance is smaller than `amount`. ```hint 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. ``` #### 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. ```hint 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. ``` #### 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 `n` exceeds 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 deducts `amount` from the source account (the money is held) but does **not** credit the target. It returns a new unique transfer id, or `None` if either account does not exist, the two ids are the same, or the source balance is smaller than `amount`. - `accept_transfer(timestamp, account_id, transfer_id)` credits the target account only if the transfer exists, is still pending, `account_id` is its target, and acceptance happens within `transfer_window` of the moment the transfer was created. It returns `True` on success and `False` otherwise. - A pending transfer that is not accepted within the window expires, and the held amount returns to the source account. ```hint 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 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. ``` #### Clarifying Questions for this Part - Is an acceptance at exactly `created_at + transfer_window` still 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 `pay` and 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 ### What a Strong Answer Covers - Each stage extends the class without rewriting earlier logic - A stated money invariant (balances plus held amounts always equal total deposits minus total payments) and code that preserves it - Concrete test sequences: pay against held funds, accept after expiry, accept by the wrong account, ties in the ranking - Time and space cost per operation ### 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?

Overview: 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.

|Home/Software Engineering Fundamentals/Circle
Circle logo
Circle
Sep 4, 2026
mediumSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
0
0

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_window is 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 Guidance

  • Should invalid calls (unknown account, insufficient funds) return a failure value or raise? This version returns None or False .
  • 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; returns False if the id already exists.
  • deposit : adds amount to the account and returns the new balance; returns None if the account does not exist.
  • pay : withdraws amount and returns the new balance; returns None if the account does not exist or its balance is smaller than amount .

What This Part Should Cover Guidance

  • 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 Guidance

  • 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 n exceeds the number of accounts?
  • Do rejected deposits or payments count?

What This Part Should Cover Guidance

  • 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 deducts amount from the source account (the money is held) but does not credit the target. It returns a new unique transfer id, or None if either account does not exist, the two ids are the same, or the source balance is smaller than amount .
  • accept_transfer(timestamp, account_id, transfer_id) credits the target account only if the transfer exists, is still pending, account_id is its target, and acceptance happens within transfer_window of the moment the transfer was created. It returns True on success and False otherwise.
  • 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 Guidance

  • Is an acceptance at exactly created_at + transfer_window still 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 Guidance

  • Hold semantics: held money is unavailable to pay and 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

What a Strong Answer Covers Guidance

  • Each stage extends the class without rewriting earlier logic
  • A stated money invariant (balances plus held amounts always equal total deposits minus total payments) and code that preserves it
  • Concrete test sequences: pay against held funds, accept after expiry, accept by the wrong account, ties in the ranking
  • Time and space cost per operation

Follow-up Questions Guidance

  • 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?
Loading comments...