Design in-memory payment and friendship system
Company: Robinhood
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Technical Screen
Quick Answer: This question evaluates object-oriented design skills, stateful in-memory data structures, request processing, and correctness constraints such as friend relationships, unique request IDs, and atomic balance transfers.
Constraints
- User ids and req_ids are strings; every send/accept/transfer req_id is globally unique (a reused req_id fails).
- Balances and amounts are integers; initial_balance >= 0 and transfer amount > 0.
- Transfers are only allowed between users whose friend request has been accepted (they are friends).
- A failed transfer must leave both balances unchanged.
- Accepting an already-accepted or unknown original_req_id fails.
Examples
Input: ([('register','alice',100),('register','bob',50),('send','r1','alice','bob'),('accept','r2','r1'),('are_friends','alice','bob'),('transfer','r3','alice','bob',30),('balance','alice'),('balance','bob')],)
Expected Output: [True, True, True, True, True, True, 70, 80]
Explanation: Happy path: register two users, send + accept a friend request (now friends), then a 30-unit transfer succeeds, moving alice 100->70 and bob 50->80.
Input: ([('register','alice',100),('register','bob',50),('transfer','r1','alice','bob',10)],)
Expected Output: [True, True, False]
Explanation: Transfer fails because alice and bob were never made friends (no accepted friend request).
Input: ([('register','alice',100),('register','bob',50),('send','r1','alice','bob'),('accept','r2','r1'),('transfer','r3','alice','bob',200)],)
Expected Output: [True, True, True, True, False]
Explanation: Friends, but the 200-unit transfer exceeds alice's balance of 100, so it fails and balances stay unchanged.
Input: ([('register','alice',100),('register','alice',999),('balance','alice')],)
Expected Output: [True, False, 100]
Explanation: Re-registering an existing user fails and does not overwrite the balance; alice keeps her original 100.
Input: ([('register','alice',100),('register','bob',50),('send','r1','alice','bob'),('accept','r2','r1'),('accept','r3','r1')],)
Expected Output: [True, True, True, True, False]
Explanation: The second accept of the same original friend request (r1) fails because it was already accepted.
Input: ([('accept','r2','nope'),('are_friends','x','y'),('balance','ghost')],)
Expected Output: [False, False, None]
Explanation: Edge cases: accepting an unknown friend-request id fails, unknown users are not friends, and balance of an unknown user is None.
Input: ([],)
Expected Output: []
Explanation: Empty edge case: no operations yields an empty result list.
Input: ([('register','a',10),('register','b',10),('send','r1','a','b'),('accept','r2','r1'),('transfer','r3','a','b',10),('transfer','r4','a','b',1),('balance','a'),('balance','b')],)
Expected Output: [True, True, True, True, True, False, 0, 20]
Explanation: Boundary: first transfer drains a's full balance to 0 (succeeds); the next 1-unit transfer fails for insufficient funds; final balances are a=0, b=20.
Hints
- Keep three maps: balances (user -> int), friends (user -> set of users), and pending (friend-request req_id -> the (user1, user2) pair). The accept op only carries an original_req_id, so you must look the pair up there.
- Track every consumed req_id in a single set so a reused id (across send/accept/transfer) deterministically fails, and a separate 'accepted' set so a friend request can't be accepted twice.
- Validate a transfer in order: fresh req_id, amount > 0, both users exist, they are friends, sufficient balance — and only mutate the two balances once all checks pass so failures leave state untouched.