PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates data-structure and algorithm design competencies with emphasis on concurrency, time-based scheduling, aggregation (TopK), transactional consistency, idempotency, and merge semantics for an in-memory banking service, falling under the Coding & Algorithms and system-design domain.

  • Medium
  • HubSpot
  • Coding & Algorithms
  • Software Engineer

Design a bank with scheduled payments and merges

Company: HubSpot

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: Medium

Interview Round: Take-home Project

Design an in-memory banking service that supports the following operations: ( 1) CreateAccount(accountId), ( 2) Deposit(accountId, amount), ( 3) Transfer(srcId, dstId, amount), ( 4) TopKByTotalOut(k) to return the k accounts with the largest cumulative outgoing amounts, ( 5) SchedulePayment(srcId, dstId, amount, executeAtEpochMillis) where a scheduled payment executes when its due time arrives and is skipped (not retried) if the source balance is insufficient, and ( 6) MergeAccounts(aId, bId) to combine two accounts into one (define the resulting accountId, combined balance, combined outgoing totals, and how to handle any scheduled-but-not-yet-executed payments for both accounts). Specify the data structures you would use to support efficient real-time updates and time-based execution (e.g., for scheduling), and describe how you would maintain the TopK view as deposits, transfers, merges, and scheduled payment executions occur. Provide clear method signatures, expected time and space complexities for each operation, and the rules for consistency and idempotency (e.g., handling duplicate requests or retries). Explain how you would process due payments continuously (or on a timer) and ensure correctness when multiple operations happen near the execution time. Discuss edge cases such as merging accounts that have pending scheduled payments, transferring to the same account, zero/negative amounts, and ties in TopK ordering.

Quick Answer: This question evaluates data-structure and algorithm design competencies with emphasis on concurrency, time-based scheduling, aggregation (TopK), transactional consistency, idempotency, and merge semantics for an in-memory banking service, falling under the Coding & Algorithms and system-design domain.

In-Memory Bank — Level 1: Accounts, Deposits, Transfers

Build an in-memory banking service and process a list of operations, returning the result of each operation in order. Each operation is a list whose first element is the op name: - `['CREATE', accountId]` — create a new account with balance 0. Returns `true` on success, `false` if the account already exists. - `['DEPOSIT', accountId, amount]` — add `amount` to the account. Returns the new balance on success, or `null` if the account does not exist or `amount <= 0`. - `['TRANSFER', srcId, dstId, amount]` — move `amount` from `srcId` to `dstId`. Returns `true` on success. Returns `false` if either account is missing, `srcId == dstId`, `amount <= 0`, or the source balance is less than `amount`. On a successful transfer, `amount` is added to the source account's cumulative outgoing total. Return an array containing the result of every operation, in input order. Focus: choose data structures (a hash map `accountId -> balance` and a parallel `accountId -> totalOut`) that give O(1) create / deposit / transfer.

Constraints

  • accountId values are strings (or any hashable id).
  • Balances and amounts are non-negative integers for valid operations.
  • A deposit or transfer with amount <= 0 is rejected.
  • Transfers to the same account (srcId == dstId) are rejected.
  • Total outgoing is incremented on the source only on a successful transfer.

Examples

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 100], ['TRANSFER', 'a', 'b', 40], ['TRANSFER', 'a', 'b', 200], ['CREATE', 'a']],)

Expected Output: [True, True, 100, True, False, False]

Explanation: Create a,b succeed. Deposit 100 into a returns new balance 100. Transfer 40 a->b succeeds. Transfer 200 fails (a only has 60). Re-creating 'a' fails (already exists).

Input: ([['DEPOSIT', 'x', 50], ['CREATE', 'x'], ['DEPOSIT', 'x', -5], ['DEPOSIT', 'x', 30], ['TRANSFER', 'x', 'x', 10]],)

Expected Output: [None, True, None, 30, False]

Explanation: Deposit before create returns null. Create x succeeds. Negative deposit returns null. Deposit 30 returns balance 30. Self-transfer is rejected (false).

Input: ([],)

Expected Output: []

Explanation: Empty operation list yields an empty result array.

Hints

  1. Keep two hash maps: one for balance, one for cumulative outgoing per account.
  2. Validate every precondition (account existence, positive amount, sufficient balance, src != dst) before mutating state.
  3. DEPOSIT returns the new balance; CREATE/TRANSFER return booleans; invalid DEPOSIT returns null.

In-Memory Bank — Level 2: Top-K by Outgoing Total

Extend Level 1 (CREATE / DEPOSIT / TRANSFER) with a ranking query. Add one operation: - `['TOPK', k]` — return a list of up to `k` account ids ordered by cumulative outgoing total, descending. Break ties by account id ascending (lexicographic). If `k` exceeds the number of accounts, return all of them. If `k <= 0`, return an empty list. Return an array containing the result of every operation. All Level 1 rules still apply (a successful TRANSFER adds `amount` to the source's outgoing total). Focus: how do you keep the Top-K view consistent as transfers continuously change outgoing totals? A full sort per query is O(A log A); discuss when a balanced BST / indexed heap keyed by (totalOut, id) would be preferable for frequent queries.

Constraints

  • All Level 1 constraints apply.
  • TOPK ordering is by outgoing total descending, then account id ascending.
  • k <= 0 returns an empty list; k larger than account count returns all accounts.

Examples

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['CREATE', 'c'], ['DEPOSIT', 'a', 100], ['DEPOSIT', 'b', 100], ['TRANSFER', 'a', 'c', 60], ['TRANSFER', 'b', 'c', 60], ['TRANSFER', 'a', 'b', 10], ['TOPK', 2]],)

Expected Output: [True, True, True, 100, 100, True, True, True, ['a', 'b']]

Explanation: a sends 60+10=70 out, b sends 60 out, c sends 0. Top 2 by outgoing: a (70) then b (60).

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['TOPK', 5], ['TOPK', 0]],)

Expected Output: [True, True, ['a', 'b'], []]

Explanation: Both accounts have outgoing 0, so the tie-break (id ascending) orders them a,b. k=5 returns all; k=0 returns empty.

Input: ([['CREATE', 'z'], ['CREATE', 'a'], ['DEPOSIT', 'z', 50], ['DEPOSIT', 'a', 50], ['TRANSFER', 'z', 'a', 20], ['TRANSFER', 'a', 'z', 20], ['TOPK', 3]],)

Expected Output: [True, True, 50, 50, True, True, ['a', 'z']]

Explanation: Both z and a have outgoing 20 — a tie. Tie-break by id ascending puts 'a' before 'z'.

Hints

  1. A successful transfer is the only event that changes an outgoing total.
  2. For the tie-break, sort by a composite key (-totalOut, accountId).
  3. If TOPK is called far more often than transfers, maintain a sorted multiset/BST keyed by (totalOut, id) and update it on each transfer instead of sorting on demand.

In-Memory Bank — Level 3: Scheduled Payments on a Timer

Extend Levels 1-2 with future-dated payments and a clock. Add two operations: - `['SCHEDULE', srcId, dstId, amount, dueTime]` — register a payment to execute when the clock reaches `dueTime`. Returns `true` if registered. Returns `false` immediately if either account is missing at schedule time, `srcId == dstId`, or `amount <= 0`. The payment is NOT executed now — only validated for registration. - `['TICK', now]` — advance the clock to `now` (never moves backward) and execute every payment whose `dueTime <= now`. Process due payments in order of (dueTime ascending, then schedule insertion order). Each due payment runs only if the source still exists, the destination still exists, they differ, and the source balance is sufficient; otherwise it is **skipped, not retried** (it is consumed). A successful execution updates balances and the source's outgoing total. `TICK` returns a list of booleans — one per payment that became due this tick — in the original schedule (insertion) order, where `true` means executed and `false` means skipped. Return an array with the result of every operation. (Conditions are re-checked at execution time, so a payment valid at schedule time may still be skipped later.) Focus: a min-heap (priority queue) keyed by (dueTime, insertionSeq) gives O(log P) scheduling and O(log P) per due payment drained — the standard timer-wheel / delayed-queue pattern.

Constraints

  • Clock is monotonic: TICK never moves time backward (use max(now, given)).
  • A due payment executes at most once; if skipped (insufficient/missing/self) it is consumed, never retried.
  • Execution preconditions are re-evaluated at TICK time, not at SCHEDULE time.
  • Due ordering: (dueTime ascending, then schedule insertion order).

Examples

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 100], ['SCHEDULE', 'a', 'b', 30, 1000], ['TICK', 500], ['TICK', 1000], ['TOPK', 1]],)

Expected Output: [True, True, 100, True, [], [True], ['a']]

Explanation: Payment due at 1000. TICK 500 is before due time, so nothing runs ([]). TICK 1000 executes it (a has 100 >= 30) -> [True]. a now has outgoing 30, leading TOPK.

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 20], ['SCHEDULE', 'a', 'b', 50, 100], ['SCHEDULE', 'a', 'b', 10, 100], ['TICK', 100]],)

Expected Output: [True, True, 20, True, True, [False, True]]

Explanation: Both due at 100. The first (amount 50) runs first by insertion order but a only has 20 -> skipped (False). The second (amount 10) then succeeds (True). Skip-not-retry: the 50 payment is consumed.

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 100], ['SCHEDULE', 'a', 'a', 10, 50], ['SCHEDULE', 'a', 'b', -5, 50], ['SCHEDULE', 'x', 'b', 5, 50], ['TICK', 50]],)

Expected Output: [True, True, 100, False, False, False, []]

Explanation: All three SCHEDULE calls are rejected at registration: self-payment (a->a), non-positive amount (-5), and missing source account (x). Nothing is queued, so TICK 50 runs nothing ([]).

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 100], ['SCHEDULE', 'a', 'b', 40, 200], ['TICK', 100], ['SCHEDULE', 'a', 'b', 40, 150], ['TICK', 250]],)

Expected Output: [True, True, 100, True, [], True, [True, True]]

Explanation: First payment due 200 not yet due at TICK 100 ([]). A second payment due 150 is scheduled. TICK 250 drains both (due 150 then due 200): a has 100, pays 40 then 40, both succeed. Flags returned in schedule (insertion) order -> [True, True].

Hints

  1. Store pending payments in a min-heap keyed by (dueTime, insertionSeq) so the earliest-due payment is always on top.
  2. On TICK, pop while the top's dueTime <= now; that drains exactly the payments that became due.
  3. Re-check balance/existence at execution time — a payment that was valid when scheduled can still be skipped if the balance dropped.
  4. Return the per-payment executed/skipped flags in original schedule order, not heap-pop order.

In-Memory Bank — Level 4: Merging Accounts

Extend Levels 1-3 with account merges. Add one operation: - `['MERGE', aId, bId]` — combine account `bId` into account `aId`. The resulting account keeps id `aId`. Its balance becomes `balance[a] + balance[b]` and its outgoing total becomes `out[a] + out[b]`. Account `bId` is removed. Any pending scheduled payment that referenced `bId` (as source or destination) is **re-pointed to `aId`** so it still executes against the merged account. Returns `true` on success, or `false` if either account is missing or `aId == bId`. All prior operations still apply. Re-pointing is done lazily: keep a `merged[bId] = aId` forwarding map and resolve any payment's src/dst to its current account at execution time. A scheduled payment that, after re-pointing, would have source == destination (because both endpoints merged into the same account) is skipped at execution time. Return an array with the result of every operation. Focus: union-find / forwarding-pointer style remapping keeps MERGE O(1) while preserving the integrity of the pending-payment heap without rebuilding it.

Constraints

  • MERGE keeps id aId; bId is removed. Balances and outgoing totals are summed.
  • MERGE fails if either account is missing or aId == bId.
  • Pending payments referencing bId are redirected to aId and resolved at execution time.
  • A payment whose resolved source equals its resolved destination is skipped at execution.
  • All Level 1-3 rules still hold (skip-not-retry, monotonic clock, tie-broken TopK).

Examples

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['DEPOSIT', 'a', 100], ['DEPOSIT', 'b', 50], ['TRANSFER', 'a', 'b', 30], ['TRANSFER', 'b', 'a', 10], ['MERGE', 'a', 'b'], ['TOPK', 5]],)

Expected Output: [True, True, 100, 50, True, True, True, ['a']]

Explanation: Before merge: out[a]=30, out[b]=10. MERGE folds b into a: balance 70+? and out[a]=30+10=40, b removed. Only account 'a' remains, so TOPK returns ['a'].

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['CREATE', 'c'], ['DEPOSIT', 'b', 100], ['SCHEDULE', 'b', 'c', 40, 100], ['MERGE', 'a', 'b'], ['TICK', 100], ['TOPK', 5]],)

Expected Output: [True, True, True, 100, True, True, [True], ['a', 'c']]

Explanation: b is scheduled to pay c, then b merges into a (b's 100 balance moves to a). At TICK 100 the payment's source b re-points to a: a (100) pays 40 to c, out[a]=40. TOPK: a (40) before c (0).

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['MERGE', 'a', 'a'], ['MERGE', 'a', 'x'], ['MERGE', 'a', 'b'], ['DEPOSIT', 'b', 10]],)

Expected Output: [True, True, False, False, True, None]

Explanation: MERGE a,a fails (same id). MERGE a,x fails (x missing). MERGE a,b succeeds and removes b. A later DEPOSIT to the now-gone 'b' returns null.

Input: ([['CREATE', 'a'], ['CREATE', 'b'], ['CREATE', 'c'], ['DEPOSIT', 'c', 100], ['SCHEDULE', 'c', 'b', 40, 100], ['MERGE', 'a', 'b'], ['TICK', 100], ['TOPK', 3]],)

Expected Output: [True, True, True, 100, True, True, [True], ['c', 'a']]

Explanation: Payment c->b is scheduled, then b merges into a. At TICK 100 the destination b re-points to a: c (100) pays 40 to a, out[c]=40. TOPK: c (40) before a (0).

Hints

  1. Don't scan the heap on MERGE — record merged[b] = a and resolve endpoints when the payment finally executes.
  2. Combine both balance and outgoing total of b into a, then delete b's entries.
  3. Follow the forwarding chain (like union-find find) so a payment scheduled before several merges still lands on the surviving account.
  4. Guard execution with src != dst after remapping: two endpoints can collapse into the same account post-merge.
Last updated: Jun 25, 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

  • Validate hiring request under role constraints - HubSpot (medium)
  • Find a special person using knows(a,b) - HubSpot (easy)
  • Design and implement a bank account system - HubSpot (Medium)
  • Design file deduplication at scale - HubSpot (Medium)
  • Implement Python LRU cache with varargs - HubSpot (Medium)