PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

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.

  • medium
  • Revolut
  • Coding & Algorithms
  • Software Engineer

Implement a thread-safe money transfer

Company: Revolut

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

## Problem: Implement a concurrent-safe transfer function You are given an in-memory store of bank accounts: - Each account has an `accountId` and a `balance` (integer cents). - Multiple threads may call `transfer(fromId, toId, amount)` concurrently. ### Task Implement `transfer(fromId, toId, amount)` such that it is: - **Correct under concurrency** (no lost updates). - **Atomic**: either the money moves from `fromId` to `toId` entirely, or nothing changes. - **Deadlock-free** under concurrent transfers between overlapping accounts. ### Rules - Validate inputs: - `amount > 0` - accounts must exist - reject/return failure on insufficient funds - The total sum of money across all accounts must remain unchanged after any successful transfer. ### Assumptions - Single process / single JVM (no distributed transactions). - You may use locks (e.g., synchronized/ReentrantLock/ReadWriteLock) or other concurrency primitives. ### Deliverable Provide the API design and implementation approach, including how you prevent deadlocks and ensure correctness.

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.

## Problem You maintain an in-memory store of bank accounts. Each account has an `accountId` and an integer `balance` (in cents). In production, many threads call `transfer(fromId, toId, amount)` concurrently, and your real implementation must be **correct under concurrency** (no lost updates), **atomic** (the money fully moves or nothing changes), and **deadlock-free** (e.g. by always acquiring the two account locks in a consistent global order — lock the smaller id first, then the larger). This console grades the **deterministic correctness core** that your locking strategy must protect: applying a sequence of transfer requests **in order** and enforcing every validation rule. Implement a function that takes the initial accounts and a list of transfer operations, applies each one, and reports the final balances and a per-operation success flag. ### Input - `accounts`: a list of `[accountId, balance]` pairs. `accountId` is a string; `balance` is a non-negative integer (cents). - `operations`: a list of `[fromId, toId, amount]` transfer requests, applied **in the given order**. ### A transfer succeeds only if ALL of these hold 1. `amount > 0` (reject zero and negative amounts). 2. Both `fromId` and `toId` exist in the store. 3. `fromId` has sufficient funds (`balance[fromId] >= amount`). On success, money moves atomically: `balance[fromId] -= amount`, `balance[toId] += amount`. On failure, **nothing changes** and the operation is recorded as failed. A self-transfer (`fromId == toId`) with a valid amount and sufficient funds is a no-op that still counts as a success (the net balance is unchanged). The total sum of money across all accounts is invariant across any successful transfer. ### Output Return `[finalAccounts, results]` where: - `finalAccounts` is the list of `[accountId, balance]` pairs in the **same order** as the input `accounts`. - `results` is a list of booleans, one per operation, in order: `true` if that transfer succeeded, `false` otherwise. ### Concurrency design (what the real, multi-threaded version must add) - Guard each account's balance with its own lock (or use one lock per account / a striped lock map). - To avoid deadlock when two threads transfer between the same overlapping accounts in opposite directions, **always acquire locks in a fixed global order** (e.g. by `accountId`), never in call order. - Read-validate-write must happen while both locks are held so the operation is atomic and free of lost updates.

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

  1. Build a dictionary from accountId to balance, but keep the original account order separately so you can emit final balances in input order.
  2. 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.
  3. 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.
Last updated: Jun 26, 2026

Related Coding Questions

  • Implement a random fixed-size load balancer - Revolut (medium)

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.