PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates a candidate's skills in system design, data modeling, transactional integrity, concurrency control, idempotency, API design, and algorithmic complexity for implementing a minimal banking service.

  • Medium
  • HubSpot
  • Coding & Algorithms
  • Software Engineer

Design and implement a bank account system

Company: HubSpot

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: Medium

Interview Round: Take-home Project

Design and implement a minimal banking service with the following capabilities: 1) Create bank accounts with unique IDs and an initial balance; support deposit and withdrawal operations while preventing overdrafts. 2) Maintain per-account transaction history. Each record should include a timestamp, operation type (deposit, withdrawal, transfer_pending, transfer_in, transfer_out), amount, and resulting balance. Provide APIs to fetch the full history and the most recent N entries. 3) Implement inter-account transfers that execute only if the recipient explicitly accepts them. A transfer starts in PENDING and does not change balances until accepted; if rejected or expired, it is canceled. Ensure idempotent accept/reject handling, validate sufficient funds at acceptance time, and make operations safe under concurrent requests. Expose clear method signatures (e.g., createAccount, deposit, withdraw, createTransfer, acceptTransfer, rejectTransfer, getHistory). Describe chosen data structures, outline time/space complexity for core operations, and discuss edge cases (duplicate requests, invalid accounts, negative amounts, and clock/timestamp considerations).

Quick Answer: This question evaluates a candidate's skills in system design, data modeling, transactional integrity, concurrency control, idempotency, API design, and algorithmic complexity for implementing a minimal banking service.

Bank Service — Level 1: Accounts, Deposit, Withdraw (Overdraft-Safe)

Implement a minimal in-memory banking service. You are given a list of `operations`; replay them in order and return a list with one result per operation. Each operation is a list whose first element is the op name: - `['create', acct_id, initial_balance]` — create an account with a unique id and a non-negative initial balance. Return `True` on success; return `False` if the id already exists or `initial_balance` is negative. - `['deposit', acct_id, amount]` — add `amount` (positive integer cents) to the account. Return the new balance, or `None` if the account does not exist or `amount <= 0`. - `['withdraw', acct_id, amount]` — subtract `amount` from the account, **preventing overdrafts**. Return the new balance, or `None` if the account does not exist, `amount <= 0`, or the balance is insufficient. - `['balance', acct_id]` — return the current balance, or `None` if the account does not exist. Money is represented as integer cents (never floats).

Constraints

  • Money is an integer number of cents; amounts for deposit/withdraw must be positive.
  • Account ids are unique strings; creating a duplicate id fails (returns False).
  • Withdrawals never overdraw the balance.
  • initial_balance must be >= 0.

Examples

Input: ([['create', 'A', 100], ['deposit', 'A', 50], ['withdraw', 'A', 30], ['balance', 'A']],)

Expected Output: [True, 150, 120, 120]

Explanation: Create with 100, +50 -> 150, -30 -> 120, balance reads 120.

Input: ([['create', 'A', 100], ['withdraw', 'A', 500]],)

Expected Output: [True, None]

Explanation: Overdraft prevented: withdrawing 500 from 100 fails and returns None.

Input: ([['create', 'A', 100], ['create', 'A', 200]],)

Expected Output: [True, False]

Explanation: Duplicate account id is rejected on the second create.

Input: ([['create', 'A', -5], ['deposit', 'B', 10], ['deposit', 'A', -10]],)

Expected Output: [False, None, None]

Explanation: Negative initial balance fails; deposit to missing account B is None; A was never created.

Input: ([],)

Expected Output: []

Explanation: No operations -> empty result list.

Input: ([['create', 'A', 0], ['withdraw', 'A', 0], ['deposit', 'A', 1000000], ['withdraw', 'A', 1000000], ['balance', 'A']],)

Expected Output: [True, None, 1000000, 0, 0]

Explanation: Zero-amount withdraw is invalid (None); large deposit then full withdraw nets to 0.

Hints

  1. Use a dict {acct_id -> balance} for O(1) access.
  2. Validate amount > 0 and (for withdraw) balance >= amount before mutating.
  3. Return None for any operation on a non-existent account.

Bank Service — Level 2: Transaction History

Extend Level 1. Every successful deposit/withdraw now appends a record to a per-account, append-only history. Each record is `[op_type, amount, resulting_balance]` where `op_type` is `'deposit'` or `'withdrawal'` and `resulting_balance` is the balance immediately AFTER the operation. New operations (Level 1 ops still apply): - `['history', acct_id]` — return the full chronological history (a list of records), or `None` if the account does not exist. - `['recent', acct_id, n]` — return the most recent `n` records (chronological order). Return `[]` when `n == 0`, `None` if the account does not exist or `n < 0`. If `n` exceeds the history length, return the whole history. History is naturally chronological (append order), so 'recent' is a slice.

Constraints

  • History is append-only and chronological (no sorting needed).
  • A record is [op_type, amount, resulting_balance]; op_type is 'deposit' or 'withdrawal'.
  • recent(acct, 0) returns [] (do NOT return the whole list — list[-0:] is a footgun).
  • history/recent on a missing account returns None; recent with n < 0 returns None.

Examples

Input: ([['create', 'A', 100], ['deposit', 'A', 50], ['withdraw', 'A', 30], ['history', 'A']],)

Expected Output: [True, 150, 120, [['deposit', 50, 150], ['withdrawal', 30, 120]]]

Explanation: Two mutations produce two chronological records with their resulting balances.

Input: ([['create', 'A', 0], ['deposit', 'A', 10], ['deposit', 'A', 20], ['deposit', 'A', 30], ['recent', 'A', 2]],)

Expected Output: [True, 10, 30, 60, [['deposit', 20, 30], ['deposit', 30, 60]]]

Explanation: recent(2) returns the last two records in chronological order.

Input: ([['create', 'A', 0], ['deposit', 'A', 10], ['recent', 'A', 0]],)

Expected Output: [True, 10, []]

Explanation: recent with n==0 returns an empty list, not the whole history.

Input: ([['create', 'A', 0], ['deposit', 'A', 10], ['recent', 'A', 5]],)

Expected Output: [True, 10, [['deposit', 10, 10]]]

Explanation: n exceeds history length, so the whole (1-record) history is returned.

Input: ([['create', 'A', 100], ['withdraw', 'A', 999], ['history', 'A']],)

Expected Output: [True, None, []]

Explanation: A failed (overdraft) withdraw does NOT append a record; history stays empty.

Input: ([['history', 'Z'], ['recent', 'Z', 3]],)

Expected Output: [None, None]

Explanation: Both history reads on a non-existent account return None.

Input: ([['create', 'A', 50], ['history', 'A'], ['recent', 'A', -1]],)

Expected Output: [True, [], None]

Explanation: Fresh account has empty history; recent with negative n returns None.

Hints

  1. Record AFTER updating the balance so resulting_balance reflects the new state.
  2. The n==0 case must be special-cased because list[-0:] equals list[0:] (the whole list).
  3. Return copies of the records so callers cannot mutate internal state.

Bank Service — Level 3: Two-Phase Transfers (Accept / Reject, Idempotent)

Extend Level 2 with inter-account transfers that move money only when the recipient explicitly accepts. Transfer ids are assigned deterministically in creation order: the first successful transfer is `'t1'`, the second `'t2'`, and so on. A transfer is a state machine: `pending -> accepted | rejected`. New operations (Level 1 & 2 ops still apply): - `['transfer', from_id, to_id, amount]` — create a PENDING transfer. Validate both accounts exist, `from_id != to_id`, and `amount > 0`; otherwise return `None`. On success, append a `['transfer_pending', amount, balance]` record to the sender (balances do NOT change yet) and return the new transfer id (e.g. `'t1'`). - `['accept', tid]` — if PENDING and the sender has sufficient funds NOW, debit the sender (`'transfer_out'` record) and credit the recipient (`'transfer_in'` record), set status ACCEPTED, return `'accepted'`. If already ACCEPTED, return `'accepted'` (idempotent, no double-credit). If REJECTED, return `'rejected'`. If sender now lacks funds, return `'insufficient_funds'` and leave it PENDING. Unknown id returns `None`. - `['reject', tid]` — if PENDING, mark REJECTED. Return the current status. Unknown id returns `None`. Re-rejecting is idempotent. - `['status', tid]` — return the transfer's status, or `None` if unknown. Funds are validated at ACCEPTANCE time, not at creation.

Constraints

  • Transfer ids are 't1','t2',... assigned in successful-creation order.
  • Creation records 'transfer_pending' but does NOT move money.
  • Funds are validated at ACCEPT time; accept on insufficient funds leaves the transfer PENDING.
  • accept/reject are idempotent: a terminal status (accepted/rejected) is returned unchanged with no double-apply.
  • Self-transfer (from == to), non-existent accounts, and amount <= 0 all return None at creation.

Examples

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 40], ['accept', 't1'], ['balance', 'A'], ['balance', 'B']],)

Expected Output: [True, True, 't1', 'accepted', 60, 40]

Explanation: Happy path: pending then accepted debits A (100->60) and credits B (0->40).

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 40], ['accept', 't1'], ['accept', 't1'], ['balance', 'A'], ['balance', 'B']],)

Expected Output: [True, True, 't1', 'accepted', 'accepted', 60, 40]

Explanation: Idempotent accept: the second accept returns 'accepted' with no double-credit.

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 40], ['reject', 't1'], ['accept', 't1'], ['balance', 'A'], ['balance', 'B']],)

Expected Output: [True, True, 't1', 'rejected', 'rejected', 100, 0]

Explanation: Rejected is terminal; a later accept returns 'rejected' and no money moves.

Input: ([['create', 'A', 30], ['create', 'B', 0], ['transfer', 'A', 'B', 50], ['accept', 't1'], ['balance', 'A']],)

Expected Output: [True, True, 't1', 'insufficient_funds', 30]

Explanation: Funds checked at accept time: A only has 30, so the transfer of 50 cannot be accepted.

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 40], ['history', 'A']],)

Expected Output: [True, True, 't1', [['transfer_pending', 40, 100]]]

Explanation: Creation records intent on the sender without changing the balance (still 100).

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 40], ['accept', 't1'], ['history', 'B']],)

Expected Output: [True, True, 't1', 'accepted', [['transfer_in', 40, 40]]]

Explanation: On accept the recipient gets a 'transfer_in' record with the new balance 40.

Input: ([['create', 'A', 100], ['transfer', 'A', 'A', 10], ['transfer', 'A', 'Z', 10], ['transfer', 'A', 'A', -5], ['accept', 't1'], ['status', 't1']],)

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

Explanation: Self-transfer, missing recipient, and negative amount all fail at creation, so no 't1' exists for accept/status.

Input: ([['create', 'A', 100], ['create', 'B', 0], ['transfer', 'A', 'B', 30], ['status', 't1'], ['reject', 't1'], ['reject', 't1'], ['status', 't1']],)

Expected Output: [True, True, 't1', 'pending', 'rejected', 'rejected', 'rejected']

Explanation: Status starts pending; reject is idempotent and the status stays rejected.

Hints

  1. Model the transfer as a tiny state machine: pending -> accepted | rejected; terminal states are returned unchanged.
  2. Check status BEFORE mutating balances so a repeated accept can't double-credit.
  3. Re-check the sender's balance at accept time — it may have dropped since creation.
Last updated: Jun 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

  • Validate hiring request under role constraints - HubSpot (medium)
  • Find a special person using knows(a,b) - HubSpot (easy)
  • Design file deduplication at scale - HubSpot (Medium)
  • Design a bank with scheduled payments and merges - HubSpot (Medium)
  • Implement a same-host web crawler - HubSpot (Medium)