PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

Implement a simplified multi-level banking system evaluates algorithm design, data structures, correctness, complexity, edge cases, and implementation details in a realistic interview setting. A strong answer states assumptions, handles edge cases, explains trade-offs, and shows how to validate the result clearly.

  • Medium
  • Circle
  • Coding & Algorithms
  • Software Engineer

Implement a simplified multi-level banking system

Company: Circle

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: Medium

Interview Round: Take-home Project

Implement a simplified banking system with multi-level requirements. All operations accept a stringified timestamp in milliseconds; timestamps are guaranteed to be unique and provided in strictly increasing order. You have access to data from the current and all previous levels. Level 1: Support creating new accounts, depositing money into accounts, and transferring money between two accounts. Level 2: Support ranking accounts based on the total value of outgoing transactions. Level 3: Allow scheduling payments and checking the status of scheduled payments. Level 4: Support merging two accounts while retaining both accounts' balances and transaction histories. You may proceed to the next level only after all tests for the current level pass.

Quick Answer: Implement a simplified multi-level banking system evaluates algorithm design, data structures, correctness, complexity, edge cases, and implementation details in a realistic interview setting. A strong answer states assumptions, handles edge cases, explains trade-offs, and shows how to validate the result clearly.

Multi-Level Banking System - Level 1: Accounts, Deposits, Transfers

Implement a simplified banking system. You are given a list of operations; each operation is a list of string tokens. Every operation includes a stringified millisecond timestamp (token index 1) that is unique and strictly increasing across operations. Return a list of string results, one per operation. Supported operations at Level 1: - ['CREATE_ACCOUNT', timestamp, accountId] -> 'true' if a new account is created, 'false' if accountId already exists. - ['DEPOSIT', timestamp, accountId, amount] -> the new balance (as a string) after depositing the integer amount, or '' if the account does not exist. - ['TRANSFER', timestamp, sourceId, targetId, amount] -> the new balance of sourceId (as a string) after moving amount from source to target. Return '' if either account is missing, source == target, or source has insufficient funds. All accounts start at balance 0.

Constraints

  • Timestamps are unique, stringified milliseconds, given in strictly increasing order.
  • Amounts are non-negative integers.
  • All accounts start with a balance of 0.
  • A transfer to the same account, with a missing endpoint, or with insufficient funds is rejected (returns '').

Examples

Input: ([['CREATE_ACCOUNT','1','account1'],['CREATE_ACCOUNT','2','account1'],['CREATE_ACCOUNT','3','account2'],['DEPOSIT','4','non-existing','2700'],['DEPOSIT','5','account1','2700'],['TRANSFER','6','account1','account2','2701'],['TRANSFER','7','account1','account2','200']],)

Expected Output: ['true', 'false', 'true', '', '2700', '', '2500']

Explanation: Duplicate create -> 'false'; deposit to missing account -> ''; over-balance transfer (2701 > 2700) -> ''; valid 200 transfer leaves account1 at 2500.

Input: ([['CREATE_ACCOUNT','1','a'],['DEPOSIT','2','a','100'],['TRANSFER','3','a','a','50']],)

Expected Output: ['true', '100', '']

Explanation: Self-transfer (source == target) is rejected.

Input: ([],)

Expected Output: []

Explanation: Empty input -> empty result list.

Input: ([['CREATE_ACCOUNT','1','a'],['CREATE_ACCOUNT','2','b'],['DEPOSIT','3','a','1000'],['TRANSFER','4','a','b','400'],['DEPOSIT','5','b','100']],)

Expected Output: ['true', 'true', '1000', '600', '500']

Explanation: After transfer a=600; b then deposits 100 on top of the received 400 -> 500.

Hints

  1. Keep a dict mapping accountId -> integer balance.
  2. CREATE_ACCOUNT must fail (return 'false') if the id already exists.
  3. TRANSFER returns the SOURCE account's balance, not the target's, and must validate funds and distinct endpoints first.

Multi-Level Banking System - Level 2: Top Spenders Ranking

Extend Level 1. The total outgoing value of an account is the sum of all amounts it has successfully sent via TRANSFER. Add: - ['TOP_SPENDERS', timestamp, n] -> a string ranking of up to n accounts by total outgoing value, highest first, ties broken alphabetically by accountId. Format each entry as 'accountId(amount)' and join with ', '. If there are no accounts, return ''. All Level 1 operations are still supported.

Constraints

  • Outgoing value counts only successful TRANSFER amounts (failed transfers do not count).
  • Ties in outgoing value are broken alphabetically by accountId.
  • n may exceed the number of accounts; return all of them in that case.

Examples

Input: ([['CREATE_ACCOUNT','1','account1'],['CREATE_ACCOUNT','2','account2'],['CREATE_ACCOUNT','3','account3'],['DEPOSIT','4','account1','10000'],['DEPOSIT','5','account2','10000'],['DEPOSIT','6','account3','10000'],['TRANSFER','7','account1','account2','3000'],['TRANSFER','8','account1','account3','2000'],['TRANSFER','9','account2','account3','4000'],['TOP_SPENDERS','10','3']],)

Expected Output: ['true', 'true', 'true', '10000', '10000', '10000', '7000', '5000', '9000', 'account1(5000), account2(4000), account3(0)']

Explanation: account1 sent 3000+2000=5000, account2 sent 4000, account3 sent 0. The third TRANSFER returns account2's balance (9000).

Input: ([['CREATE_ACCOUNT','1','a'],['CREATE_ACCOUNT','2','b'],['TOP_SPENDERS','3','5']],)

Expected Output: ['true', 'true', 'a(0), b(0)']

Explanation: No spending yet; ties broken alphabetically; n exceeds account count.

Input: ([['TOP_SPENDERS','1','2']],)

Expected Output: ['']

Explanation: No accounts -> empty string.

Input: ([['CREATE_ACCOUNT','1','z'],['CREATE_ACCOUNT','2','y'],['CREATE_ACCOUNT','3','x'],['DEPOSIT','4','z','100'],['DEPOSIT','5','y','100'],['DEPOSIT','6','x','100'],['TRANSFER','7','z','y','50'],['TRANSFER','8','y','x','50'],['TRANSFER','9','x','z','50'],['TOP_SPENDERS','10','2']],)

Expected Output: ['true', 'true', 'true', '100', '100', '100', '50', '100', '100', 'x(50), y(50)']

Explanation: All three spent 50; alphabetical tie-break yields x, y for the top 2.

Hints

  1. Maintain a separate outgoing-total map updated only on a successful TRANSFER.
  2. Sort by (-outgoing, accountId) so higher spenders come first and ties are alphabetical.
  3. Format is exactly 'id(amount)' joined by ', '.

Multi-Level Banking System - Level 3: Scheduled Payments

Extend Levels 1-2. Add scheduled payments that execute after a delay: - ['SCHEDULE_PAYMENT', timestamp, accountId, amount, delay] -> a payment id 'payment1', 'payment2', ... (a global counter incremented per scheduled payment). The payment is due at executeAt = timestamp + delay. Return '' if the account does not exist. - ['CANCEL_PAYMENT', timestamp, accountId, paymentId] -> 'true' if a PENDING payment owned by accountId is canceled, else 'false'. - ['GET_PAYMENT_STATUS', timestamp, accountId, paymentId] -> 'PENDING', 'PAID', or 'CANCELED'; '' if the account or payment does not exist or the payment is not owned by accountId. Processing rule: before handling each operation, every PENDING payment whose executeAt <= the current timestamp is executed in scheduling order. A payment executes only if the account currently has enough balance; its amount is debited and added to the account's outgoing total (so it counts for TOP_SPENDERS), then the payment becomes PAID. A payment AT its exact executeAt timestamp executes when that timestamp's operation is processed.

Constraints

  • Payment ids are a global increasing sequence 'payment1', 'payment2', ...
  • Due payments are processed before each operation, in scheduling order, and only if the account can cover them.
  • A payment exactly at its executeAt timestamp executes when that timestamp is reached.
  • CANCEL only succeeds on a still-PENDING payment owned by the account.

Examples

Input: ([['CREATE_ACCOUNT','1','a'],['DEPOSIT','2','a','1000'],['SCHEDULE_PAYMENT','3','a','300','10'],['GET_PAYMENT_STATUS','4','a','payment1'],['DEPOSIT','11','a','0'],['GET_PAYMENT_STATUS','12','a','payment1']],)

Expected Output: ['true', '1000', 'payment1', 'PENDING', '1000', 'PENDING']

Explanation: Payment due at 3+10=13; queries at ts 11 and 12 are before that, so it stays PENDING and the balance is untouched.

Input: ([['CREATE_ACCOUNT','1','a'],['DEPOSIT','2','a','1000'],['SCHEDULE_PAYMENT','3','a','300','10'],['DEPOSIT','20','a','0'],['GET_PAYMENT_STATUS','21','a','payment1']],)

Expected Output: ['true', '1000', 'payment1', '700', 'PAID']

Explanation: By ts=20 (>=13) the payment executes: balance 1000-300=700, status PAID.

Input: ([['CREATE_ACCOUNT','1','a'],['DEPOSIT','2','a','1000'],['SCHEDULE_PAYMENT','3','a','300','100'],['CANCEL_PAYMENT','4','a','payment1'],['GET_PAYMENT_STATUS','5','a','payment1'],['CANCEL_PAYMENT','6','a','payment1']],)

Expected Output: ['true', '1000', 'payment1', 'true', 'CANCELED', 'false']

Explanation: Canceling a PENDING payment succeeds; re-canceling an already-CANCELED payment returns 'false'.

Input: ([['SCHEDULE_PAYMENT','1','nope','100','5'],['GET_PAYMENT_STATUS','2','nope','payment1']],)

Expected Output: ['', '']

Explanation: Scheduling against a missing account returns ''; no payment is created.

Hints

  1. Process due payments (executeAt <= now) at the start of every operation so balances and outgoing totals stay current.
  2. Use a global counter for payment ids; store each payment's owner, amount, executeAt, and status.
  3. A payment that cannot be afforded at execute time still transitions to PAID in this model but debits nothing.

Multi-Level Banking System - Level 4: Merge Accounts

Extend Levels 1-3. Add merging: - ['MERGE_ACCOUNTS', timestamp, accountId1, accountId2] -> 'true' if accountId2 is merged into accountId1, else 'false'. Fails (returns 'false') if either account is missing or accountId1 == accountId2. On a successful merge: accountId1's balance becomes the sum of both balances; its outgoing total becomes the sum of both outgoing totals (so prior spending of both is retained for TOP_SPENDERS); every scheduled payment that belonged to accountId2 is reassigned to accountId1 (so it later executes against accountId1's balance); accountId2 is then removed and no longer exists for any operation. All Level 1-3 operations are still supported, including due-payment processing at the start of each operation.

Constraints

  • MERGE fails if either account is missing or the two ids are equal.
  • Merged balance and outgoing total are the sums of the two accounts'.
  • Scheduled payments of the absorbed account are reassigned to the surviving account and execute against its balance.
  • After a merge, the absorbed account id is removed and any later operation referencing it behaves as if it does not exist.

Examples

Input: ([['CREATE_ACCOUNT','1','a'],['CREATE_ACCOUNT','2','b'],['DEPOSIT','3','a','1000'],['DEPOSIT','4','b','500'],['TRANSFER','5','a','b','300'],['TRANSFER','6','b','a','100'],['MERGE_ACCOUNTS','7','a','b'],['DEPOSIT','8','a','0'],['TOP_SPENDERS','9','5']],)

Expected Output: ['true', 'true', '1000', '500', '700', '700', 'true', '1500', 'a(400)']

Explanation: Before merge a=700 (sent 300), b=700 (sent 100). Merge: a balance 700+700=1500, outgoing 300+100=400; b removed.

Input: ([['CREATE_ACCOUNT','1','a'],['CREATE_ACCOUNT','2','b'],['DEPOSIT','3','b','1000'],['SCHEDULE_PAYMENT','4','b','400','10'],['MERGE_ACCOUNTS','5','a','b'],['DEPOSIT','20','a','0'],['GET_PAYMENT_STATUS','21','a','payment1']],)

Expected Output: ['true', 'true', '1000', 'payment1', 'true', '600', 'PAID']

Explanation: b's payment (due at 14) is reassigned to a on merge; by ts=20 it executes against a: 1000-400=600, PAID, and is now owned by a.

Input: ([['CREATE_ACCOUNT','1','a'],['MERGE_ACCOUNTS','2','a','a'],['MERGE_ACCOUNTS','3','a','ghost']],)

Expected Output: ['true', 'false', 'false']

Explanation: Merging an account into itself and merging a missing account both return 'false'.

Input: ([['CREATE_ACCOUNT','1','x'],['CREATE_ACCOUNT','2','y'],['DEPOSIT','3','x','100'],['DEPOSIT','4','y','200'],['MERGE_ACCOUNTS','5','x','y'],['GET_PAYMENT_STATUS','6','y','payment1'],['DEPOSIT','7','y','50']],)

Expected Output: ['true', 'true', '100', '200', 'true', '', '']

Explanation: After y is merged into x, any operation referencing y (status check, deposit) returns '' since y no longer exists.

Hints

  1. Fold accountId2 into accountId1: add balances, add outgoing totals, repoint its pending payments, then delete accountId2.
  2. Reassign the absorbed account's PENDING payments by changing their owner so future due-processing debits the survivor.
  3. Guard the no-op and missing-account cases up front and return 'false'.
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

  • Implement Recipe Storage CRUD - Circle (hard)
  • Design payment scheduler with cancel and top-K outgoing - Circle (Medium)
  • Implement cheapest itinerary with date filters - Circle (Medium)