Design an in-memory banking system
Company: Coinbase
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
Quick Answer: This question evaluates the ability to design and implement a stateful in-memory banking system, testing skills in data structures, algorithms, API design, transaction semantics, scheduling, and correctness under edge cases within the coding and algorithms domain, and it focuses primarily on practical application with conceptual system-design considerations. It is commonly asked to assess reasoning about state management, invariants, error handling, efficiency (e.g., top-k ranking), time-based scheduled events and cancellation semantics, and preserving operation history during merges, highlighting attention to edge cases and performance trade-offs.
Part 1: Account Creation and Money Movement
Constraints
- 0 <= len(operations) <= 200000
- 1 <= len(userId) <= 32
- User IDs contain only letters, digits, and underscores
- Amounts fit in a signed 64-bit integer
- All operations are processed sequentially in a single thread
Examples
Input: ([['addUser', 'alice'], ['addUser', 'bob'], ['deposit', 'alice', '100'], ['transfer', 'alice', 'bob', '30'], ['getBalance', 'alice'], ['getBalance', 'bob']],)
Expected Output: ['true', 'true', '100', '70', '70', '30']
Explanation: Alice deposits 100 and transfers 30 to Bob, leaving Alice with 70 and Bob with 30.
Input: ([['addUser', 'alice'], ['addUser', 'alice'], ['deposit', 'charlie', '10'], ['deposit', 'alice', '0'], ['addUser', 'bob'], ['transfer', 'alice', 'bob', '5'], ['deposit', 'alice', '5'], ['transfer', 'alice', 'alice', '1'], ['transfer', 'alice', 'bob', '5'], ['getBalance', 'bob']],)
Expected Output: ['true', 'false', 'error', 'error', 'true', 'error', '5', 'error', '0', '5']
Explanation: Duplicate creation, unknown users, zero deposits, insufficient funds, and self-transfer are rejected.
Hints
- A hash map from user ID to current balance is enough for this part.
- Validate all preconditions before mutating balances during a transfer.
Part 2: Rank Accounts by Total Spending
Constraints
- 0 <= len(operations) <= 200000
- 0 <= k <= number of users may be requested; if k is larger, return all users
- Amounts are positive integer strings for valid money-movement operations
- User IDs contain only letters, digits, and underscores
- Only successful outgoing transfers increase spending
Examples
Input: ([['addUser', 'alice'], ['addUser', 'bob'], ['addUser', 'carl'], ['deposit', 'alice', '100'], ['deposit', 'bob', '100'], ['transfer', 'alice', 'bob', '40'], ['transfer', 'bob', 'carl', '70'], ['getTopSpenders', '2']],)
Expected Output: ['true', 'true', 'true', '100', '100', '60', '70', 'bob:70,alice:40']
Explanation: Bob spent 70 and Alice spent 40, so Bob ranks first.
Input: ([['addUser', 'bob'], ['addUser', 'ann'], ['addUser', 'zoe'], ['deposit', 'bob', '10'], ['deposit', 'ann', '10'], ['transfer', 'bob', 'zoe', '5'], ['transfer', 'ann', 'zoe', '5'], ['getTopSpenders', '3']],)
Expected Output: ['true', 'true', 'true', '10', '10', '5', '5', 'ann:5,bob:5,zoe:0']
Explanation: Ann and Bob tie at 5, so lexicographic order puts ann before bob.
Hints
- Maintain a second map: user ID to total outgoing successful transfer amount.
- For each ranking query, sort by (-spending, userId), or use a heap if you only need the first k.
Part 3: Scheduled Payments with Cancellation
Constraints
- 0 <= len(operations) <= 200000
- processPaymentsUpTo timestamps must be non-decreasing; a backward timestamp returns 'error'
- schedulePayment requires executeAtTimestamp to be strictly greater than the current time
- Amounts must be positive integers
- Payments execute in order of (executeAtTimestamp, creationOrder)
Examples
Input: ([['addUser', 'a'], ['addUser', 'b'], ['deposit', 'a', '100'], ['schedulePayment', 'a', 'b', '40', '10'], ['processPaymentsUpTo', '5'], ['getBalance', 'a'], ['processPaymentsUpTo', '10'], ['getBalance', 'a'], ['getBalance', 'b']],)
Expected Output: ['true', 'true', '100', 'p1', '', '100', 'p1:executed', '60', '40']
Explanation: The payment is not due at time 5, then executes at time 10.
Input: ([['addUser', 'a'], ['addUser', 'b'], ['deposit', 'a', '50'], ['schedulePayment', 'a', 'b', '25', '5'], ['cancelPayment', 'p1'], ['processPaymentsUpTo', '10'], ['getBalance', 'a'], ['getBalance', 'b'], ['cancelPayment', 'p1']],)
Expected Output: ['true', 'true', '50', 'p1', 'true', '', '50', '0', 'false']
Explanation: Canceled payment p1 is skipped and cannot be canceled again.
Hints
- Store scheduled payments in a min-heap ordered by execution timestamp and creation order.
- Keep a payment-status map so canceled payments can remain in the heap and be skipped later.
Part 4: Merge Users While Preserving History
Constraints
- 0 <= len(operations) <= 50000
- User IDs are unique forever: a deleted merged source ID cannot be created again
- processPaymentsUpTo timestamps must be non-decreasing; a backward timestamp returns 'error'
- schedulePayment requires executeAtTimestamp to be strictly greater than current time
- User IDs contain only letters, digits, and underscores, so they do not contain formatting delimiters
Examples
Input: ([['addUser', 'alice'], ['addUser', 'bob'], ['deposit', 'alice', '100'], ['deposit', 'bob', '30'], ['transfer', 'alice', 'bob', '40'], ['mergeUsers', 'bob', 'alice'], ['getBalance', 'bob'], ['getBalance', 'alice'], ['getHistory', 'alice'], ['getHistory', 'bob']],)
Expected Output: ['true', 'true', '100', '30', '60', 'true', 'error', '130', 'deposit:alice:100;deposit:bob:30;transfer:alice->bob:40;merge:bob->alice', 'error']
Explanation: Bob is deleted, Alice receives Bob's balance, and Alice's history contains both accounts' prior events without duplicating their shared transfer.
Input: ([['addUser', 'a'], ['addUser', 'b'], ['addUser', 'c'], ['deposit', 'a', '100'], ['deposit', 'b', '50'], ['schedulePayment', 'b', 'c', '30', '10'], ['schedulePayment', 'a', 'b', '20', '10'], ['schedulePayment', 'b', 'a', '5', '10'], ['mergeUsers', 'b', 'a'], ['processPaymentsUpTo', '10'], ['getBalance', 'a'], ['getBalance', 'c'], ['cancelPayment', 'p2'], ['getHistory', 'a']],)
Expected Output: ['true', 'true', 'true', '100', '50', 'p1', 'p2', 'p3', 'true', 'p1:executed', '120', '30', 'false', 'deposit:a:100;deposit:b:50;merge:b->a;payment:p1:a->c:30']
Explanation: b is merged into a. Payment p1 is rewritten from b->c to a->c and executes. p2 and p3 become self-payments and are automatically canceled.
Hints
- Store each user's history as a list of chronological event records; during merge, union the two histories and deduplicate shared transfer records.
- When merging, examine pending scheduled payments and replace source with target in their endpoints.