PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates object-oriented design and stateful system skills, including time-ordered event processing, scheduling and cancellation semantics, balance accounting, and top-k aggregation using appropriate data structures.

  • medium
  • Meta
  • Coding & Algorithms
  • Software Engineer

Design a bank system with scheduled transfers

Company: Meta

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Take-home Project

## Bank System (OOD): Accounts, Transfers, Scheduling, Top Spending, Merge Design an in-memory banking system that supports: - account creation - deposits/withdrawals - immediate transfers - scheduled (delayed) transfers with cancellation - querying top spenders - merging accounts ### Core rules - Each account has a balance (integer, starts at 0). - Each operation is called with a `timestamp`. - Before executing any operation at time `t`, the system must **process all scheduled transfers** whose execution time `<= t`. - “Spending” for a customer is the total money sent out (withdrawals + transfers out, including scheduled transfers once executed; depending on your interpretation, you may also count scheduled transfers as spending when reserved—state your choice and be consistent). ### API 1. `create_account(timestamp, customer_id) -> bool` - Create if not exists. 2. `deposit(timestamp, customer_id, amount) -> bool` - Fail if account doesn’t exist. 3. `withdraw(timestamp, customer_id, amount) -> bool` - Fail if account doesn’t exist or insufficient funds. - Counts toward spending. 4. `transfer(timestamp, from_id, to_id, amount) -> bool` - Immediate transfer. - Fail if either account missing or insufficient funds. - Counts `amount` toward `from_id` spending. 5. `top_spending(timestamp, n) -> list<string>` - Return top `n` customers formatted `"customerId(total)"`. - Sort by total spending desc, then customerId asc. 6. `schedule_transfer(timestamp, from_id, to_id, amount, delay) -> string | null` - Create a transfer that will execute at `timestamp + delay`. - If invalid (missing accounts or insufficient funds), return `null`. - Return a unique `transfer_id` for cancellation. - Funds may be “reserved” immediately or deducted at execution; choose one and document it. 7. `cancel_transfer(timestamp, customer_id, transfer_id) -> bool` - Cancel only if: - `transfer_id` exists - status is still pending (not executed) - the `customer_id` is the sender - Return whether cancellation succeeded. 8. `merge_account(timestamp, from_id, to_id) -> bool` - Merge `from_id` into `to_id`: - Move remaining balance into `to_id` - Add spending totals - Update any **pending** scheduled transfers that reference `from_id` as sender or receiver to reference `to_id` - Delete `from_id` ### Notes - Assume up to ~1e5 operations. - You may use a min-heap/priority queue for scheduled transfers.

Quick Answer: This question evaluates object-oriented design and stateful system skills, including time-ordered event processing, scheduling and cancellation semantics, balance accounting, and top-k aggregation using appropriate data structures.

Implement an in-memory banking simulator. Write `solution(operations)` that processes the operations in order and returns a list of results, one per operation. Each operation is a tuple or list whose first element is the operation name: - `('create_account', timestamp, customer_id)` -> `bool` - `('deposit', timestamp, customer_id, amount)` -> `bool` - `('withdraw', timestamp, customer_id, amount)` -> `bool` - `('transfer', timestamp, from_id, to_id, amount)` -> `bool` - `('top_spending', timestamp, n)` -> `list[str]` - `('schedule_transfer', timestamp, from_id, to_id, amount, delay)` -> `str | None` - `('cancel_transfer', timestamp, customer_id, transfer_id)` -> `bool` - `('merge_account', timestamp, from_id, to_id)` -> `bool` Rules: - Every account starts with balance `0`. - Before executing any operation at time `t`, first execute all pending scheduled transfers with execution time `<= t`. - This problem uses **reservation semantics** for scheduled transfers: when a scheduled transfer is created successfully, its amount is removed from the sender immediately. - If a pending scheduled transfer is canceled, that reserved amount is refunded to the sender's current live account. - A scheduled transfer increases spending only when it actually executes. - If merges cause a pending scheduled transfer's sender and receiver to become the same live account, execution simply returns the reserved money to that account and does **not** increase spending. - `top_spending` considers only current live accounts and returns at most `n` strings formatted as `"customerId(total)"`, sorted by total spending descending, then `customer_id` ascending. - Successful scheduled transfers must return deterministic IDs: `'transfer1'`, `'transfer2'`, ... in order of successful scheduling. - A merged-away ID is deleted and cannot be created again. Return the result of every operation in order.

Constraints

  • `1 <= len(operations) <= 10^5`
  • Timestamps are integers in non-decreasing order.
  • `customer_id`, `from_id`, and `to_id` are non-empty strings and IDs are never reused after creation.
  • Amounts are positive integers; `delay` and `n` are non-negative integers.

Examples

Input: [('create_account', 1, 'alice'), ('create_account', 1, 'bob'), ('deposit', 2, 'alice', 100), ('transfer', 3, 'alice', 'bob', 30), ('withdraw', 4, 'bob', 10), ('top_spending', 5, 2)]

Expected Output: [True, True, True, True, True, ['alice(30)', 'bob(10)']]

Explanation: Alice sends 30 to Bob, and Bob withdraws 10. Spending totals are Alice=30 and Bob=10.

Input: [('create_account', 1, 'ann'), ('create_account', 1, 'ben'), ('deposit', 2, 'ann', 50), ('schedule_transfer', 3, 'ann', 'ben', 20, 5), ('top_spending', 4, 2), ('cancel_transfer', 7, 'ann', 'transfer1'), ('top_spending', 9, 2)]

Expected Output: [True, True, True, 'transfer1', ['ann(0)', 'ben(0)'], True, ['ann(0)', 'ben(0)']]

Explanation: The scheduled transfer reserves 20 immediately, but spending stays 0 until execution. It is canceled before time 8, so the money is refunded and no spending is added.

Input: [('create_account', 1, 'A'), ('create_account', 1, 'B'), ('create_account', 1, 'C'), ('deposit', 2, 'A', 100), ('schedule_transfer', 5, 'A', 'B', 40, 5), ('merge_account', 6, 'B', 'C'), ('merge_account', 7, 'A', 'C'), ('top_spending', 10, 3)]

Expected Output: [True, True, True, True, 'transfer1', True, True, ['C(0)']]

Explanation: The pending transfer originally A->B becomes C->C after both merges. At time 10 it executes by returning the reserved 40 to C, so there is no net transfer out and spending remains 0.

Input: [('create_account', 1, 'x'), ('deposit', 1, 'x', 10), ('schedule_transfer', 1, 'x', 'x', 5, 0), ('top_spending', 1, 1), ('create_account', 2, 'x'), ('withdraw', 3, 'y', 1), ('schedule_transfer', 4, 'x', 'missing', 1, 1)]

Expected Output: [True, True, 'transfer1', ['x(0)'], False, False, None]

Explanation: The zero-delay transfer is not executed during its own scheduling call; it is processed before the next operation at the same timestamp. Duplicate creation fails, withdrawing from a missing account fails, and scheduling to a missing receiver returns None.

Solution

def solution(operations):
    import heapq

    balances = {}
    spending = {}
    version = {}
    parent = {}
    all_ids = set()

    scheduled_heap = []
    spend_heap = []
    transfers = {}

    next_transfer_num = 1
    schedule_seq = 0
    results = []

    def find(x):
        path = []
        while x in parent:
            path.append(x)
            x = parent[x]
        for node in path:
            parent[node] = x
        return x

    def push_spending(customer_id):
        heapq.heappush(spend_heap, (-spending[customer_id], customer_id, version[customer_id]))

    def process_due(current_time):
        while scheduled_heap and scheduled_heap[0][0] <= current_time:
            _, _, transfer_id = heapq.heappop(scheduled_heap)
            rec = transfers.get(transfer_id)
            if rec is None or rec['status'] != 'pending':
                continue

            sender = find(rec['from'])
            receiver = find(rec['to'])
            amount = rec['amount']

            if sender == receiver:
                balances[sender] += amount
            else:
                balances[receiver] += amount
                spending[sender] += amount
                version[sender] += 1
                push_spending(sender)

            rec['status'] = 'executed'

    for op in operations:
        name = op[0]
        timestamp = op[1]
        process_due(timestamp)

        if name == 'create_account':
            _, _, customer_id = op
            if customer_id in all_ids:
                results.append(False)
            else:
                all_ids.add(customer_id)
                balances[customer_id] = 0
                spending[customer_id] = 0
                version[customer_id] = 0
                push_spending(customer_id)
                results.append(True)

        elif name == 'deposit':
            _, _, customer_id, amount = op
            if customer_id not in balances:
                results.append(False)
            else:
                balances[customer_id] += amount
                results.append(True)

        elif name == 'withdraw':
            _, _, customer_id, amount = op
            if customer_id not in balances or balances[customer_id] < amount:
                results.append(False)
            else:
                balances[customer_id] -= amount
                spending[customer_id] += amount
                version[customer_id] += 1
                push_spending(customer_id)
                results.append(True)

        elif name == 'transfer':
            _, _, from_id, to_id, amount = op
            if from_id not in balances or to_id not in balances or balances[from_id] < amount:
                results.append(False)
            elif from_id == to_id:
                results.append(True)
            else:
                balances[from_id] -= amount
                balances[to_id] += amount
                spending[from_id] += amount
                version[from_id] += 1
                push_spending(from_id)
                results.append(True)

        elif name == 'top_spending':
            _, _, n = op
            taken = []
            answer = []

            while len(answer) < n and spend_heap:
                neg_total, customer_id, ver = heapq.heappop(spend_heap)

                if customer_id not in balances:
                    continue
                if ver != version[customer_id]:
                    continue
                if -neg_total != spending[customer_id]:
                    continue

                taken.append((neg_total, customer_id, ver))
                answer.append(f'{customer_id}({spending[customer_id]})')

            for item in taken:
                heapq.heappush(spend_heap, item)

            results.append(answer)

        elif name == 'schedule_transfer':
            _, _, from_id, to_id, amount, delay = op
            if from_id not in balances or to_id not in balances or balances[from_id] < amount:
                results.append(None)
            else:
                transfer_id = f'transfer{next_transfer_num}'
                next_transfer_num += 1
                schedule_seq += 1

                balances[from_id] -= amount
                transfers[transfer_id] = {
                    'from': from_id,
                    'to': to_id,
                    'amount': amount,
                    'status': 'pending'
                }
                heapq.heappush(scheduled_heap, (timestamp + delay, schedule_seq, transfer_id))
                results.append(transfer_id)

        elif name == 'cancel_transfer':
            _, _, customer_id, transfer_id = op
            rec = transfers.get(transfer_id)

            if customer_id not in balances or rec is None or rec['status'] != 'pending':
                results.append(False)
            else:
                current_sender = find(rec['from'])
                if current_sender != customer_id:
                    results.append(False)
                else:
                    balances[current_sender] += rec['amount']
                    rec['status'] = 'canceled'
                    results.append(True)

        elif name == 'merge_account':
            _, _, from_id, to_id = op
            if from_id not in balances or to_id not in balances or from_id == to_id:
                results.append(False)
            else:
                balances[to_id] += balances[from_id]
                spending[to_id] += spending[from_id]
                version[to_id] += 1
                push_spending(to_id)

                parent[from_id] = to_id
                del balances[from_id]
                del spending[from_id]
                del version[from_id]
                results.append(True)

        else:
            raise ValueError('Unknown operation')

    return results
Explanation
The solution keeps live-account state in three dicts — `balances`, `spending`, and `version` — and processes operations in order. **Time-ordered scheduling.** A min-heap `scheduled_heap` holds `(exec_time, seq, transfer_id)`. At the start of *every* op, `process_due(timestamp)` pops all transfers due at `<= t` and settles them before the current op runs, satisfying the "execute pending first" rule. **Reservation semantics.** On `schedule_transfer`, if the sender can afford it, the amount is debited immediately and a `pending` record is stored. `cancel_transfer` refunds the reserved amount to the sender's *current* live account and marks it `canceled`. Settlement only counts spending when sender and receiver are distinct live accounts. **Merges via union-find.** `merge_account` folds `from_id` into `to_id` (summing balances/spending), deletes the source from all dicts, and sets `parent[from_id] = to_id`. `find` (with path compression) resolves any old ID to its live owner. When a scheduled transfer executes, `find` re-resolves both endpoints; if they collapse to the same account, the money is simply returned with no spending — matching the spec. **`top_spending` via a lazy heap.** Each spending change pushes `(-total, id, version)` onto `spend_heap` and bumps `version[id]`. A query pops entries, skipping any whose id is dead, whose `version` is stale, or whose stored total no longer matches — guaranteeing the popped entry reflects current state. Popped survivors are collected and pushed back. The `(-total, id)` ordering gives descending spend, ties broken by ascending id. Determinism comes from `next_transfer_num` (transfer IDs) and `schedule_seq` (heap tie-break), so equal-time transfers settle in scheduling order.

Time complexity: O((q + r) log q), where q is the number of operations and r is the total number of names returned across all top_spending calls. Each op does O(log q) heap work; top_spending pops/repushes one entry per returned name (stale entries are discarded permanently, amortizing to O(log q) each); union-find find is near-O(1) amortized with path compression.. Space complexity: O(q) — the dicts hold at most one entry per account, and both heaps plus the transfers map grow at most linearly in the number of operations..

Hints

  1. A min-heap keyed by execution time is a natural way to process all scheduled transfers due before the current operation.
  2. Do not scan every pending transfer during `merge_account`. Instead, keep a mapping from old IDs to current live IDs and resolve them lazily when a scheduled transfer executes or is canceled.
Last updated: Apr 26, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,000+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Find Shortest Unique Prefixes - Meta (medium)
  • Compute Exclusive Execution Times - Meta (medium)
  • Solve Tree Columns And Maze Variants - Meta (medium)
  • Solve Tree Diameter and Palindromic Counts - Meta (medium)
  • Simulate Monster Team Battles - Meta (hard)