Implement a thread-safe money transfer
Company: Revolut
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates a candidate's mastery of concurrent programming and data-integrity principles, focusing on thread safety, atomic state updates, and deadlock avoidance when transferring funds between shared accounts.
Constraints
- accountId values are unique strings.
- Initial balances are non-negative integers (cents).
- amount may be any integer; only amount > 0 is a valid transfer.
- Operations are applied strictly in the order given.
- A transfer referencing a missing account fails and changes nothing.
- The total sum of all balances is invariant across any successful transfer.
Examples
Input: ([['A', 100], ['B', 50]], [['A', 'B', 30]])
Expected Output: [[['A', 70], ['B', 80]], [True]]
Explanation: A valid transfer of 30 from A to B: A drops to 70, B rises to 80, total 150 unchanged.
Input: ([['A', 100], ['B', 50]], [['A', 'B', 200]])
Expected Output: [[['A', 100], ['B', 50]], [False]]
Explanation: Insufficient funds (A has only 100), so the transfer fails and nothing changes.
Input: ([['A', 100], ['B', 50]], [['A', 'B', 0], ['A', 'B', -10]])
Expected Output: [[['A', 100], ['B', 50]], [False, False]]
Explanation: Zero and negative amounts are both rejected; balances stay put.
Input: ([['A', 100]], [['A', 'C', 10]])
Expected Output: [[['A', 100]], [False]]
Explanation: Destination account C does not exist, so the transfer fails.
Input: ([['A', 100], ['B', 0], ['C', 0]], [['A', 'B', 40], ['B', 'C', 40], ['C', 'A', 40]])
Expected Output: [[['A', 100], ['B', 0], ['C', 0]], [True, True, True]]
Explanation: A cycle of transfers all succeed; money circulates A->B->C->A and every account returns to its start. Total stays 100.
Input: ([['A', 100], ['B', 50]], [['A', 'A', 30]])
Expected Output: [[['A', 100], ['B', 50]], [True]]
Explanation: A self-transfer with valid amount and sufficient funds is a no-op that still succeeds.
Input: ([], [['A', 'B', 10]])
Expected Output: [[], [False]]
Explanation: Empty store: neither account exists, so the transfer fails and the final account list is empty.
Input: ([['A', 100], ['B', 50]], [['A', 'B', 100], ['A', 'B', 1]])
Expected Output: [[['A', 0], ['B', 150]], [True, False]]
Explanation: First transfer drains A to 0 (success); the second then fails on insufficient funds. Order matters.
Hints
- Build a dictionary from accountId to balance, but keep the original account order separately so you can emit final balances in input order.
- Validate in three guards before mutating: amount > 0, both ids present, and source has enough funds. Any failure appends false and leaves all balances untouched.
- For the real concurrent implementation, the key insight is deadlock avoidance: never lock accounts in call order. Sort the two account ids and always lock the smaller one first, then the larger, so two opposing transfers can't each hold one lock and wait on the other.