Katkat

Make a Racy BankAccount Deposit Thread-Safe Without Editing the Class, in Python and Go

xAISoftware Engineer · Onsite

Last updated

A concurrency question built around a BankAccount class whose deposit method reads the balance, sleeps, then writes it back. Candidates find every race, make deposits thread-safe through a subclass or wrapper without editing or copying the class, and carry the fix over to Go, which has no inheritance.

During a short screening call, the interviewer pastes the following class and tells you that several threads call deposit on the same account object at the same time:

import time

class BankAccount:
 def __init__(self, balance=0):
 self.balance = balance

 def deposit(self, amount):
 current = self.balance # read
 time.sleep(0.1) # simulated processing delay
 self.balance = current + amount # write

Find every concurrency bug in it, make deposits thread-safe without editing this class, and then explain how you would do the same in Go.

Constraints and Clarifications

  • You may not modify BankAccount itself. Copying its body into a new class and editing the copy is also rejected: the thread-safe version must reuse the existing deposit through a subclass or a wrapper.
  • The time.sleep(0.1) stands in for real work between the read and the write. Treat it as part of the method you cannot change.
  • All threads run in one process and share the same account object.

Clarifying Questions

  • Does the class have other methods, such as a withdrawal or a balance read, that must also be safe, or only deposit?
  • Should each account be protected separately, or is one lock shared by all accounts acceptable?
  • Is the concurrency limited to threads in one process, or can several processes or hosts update the same balance?
  • Can amount be zero or negative, and should the safe version validate it?
  • May the safe version extend the public interface, for example with a method that returns the balance?

Part 1 — Find the bugs

Name every concurrency defect in deposit and its root cause. Then describe a concrete run with two threads, starting from a balance of 0, that ends with a wrong balance, and state the expected and actual results.

What This Part Should Cover

  • The root cause, kept separate from the factor that only makes the failure more likely
  • A concrete two-thread interleaving with the expected and the actual final balance
  • Whether deleting the delay would make the method correct, and why

Part 2 — Make it thread-safe without editing the class

Write the thread-safe version under the constraints above. Show where the synchronization state lives, when it is created, and exactly which statements it protects. Explain how a caller should read the balance, and how you would demonstrate that the fix works.

What This Part Should Cover

  • Reuse of the original method through a subclass or a wrapper rather than a copy
  • Ownership of the synchronization state and the scope of the critical section
  • Safe reads of the balance
  • A test that fails reliably before the fix and passes after it

Part 3 — The Go version

The same account in Go:

type BankAccount struct {
 Balance int
}

func (a *BankAccount) Deposit(amount int) {
 current := a.Balance
 time.Sleep(100 * time.Millisecond)
 a.Balance = current + amount
}

Go has no inheritance. Build the thread-safe equivalent without editing BankAccount, and explain what can go wrong silently.

What This Part Should Cover

  • The Go mechanism used in place of subclassing, and its method-resolution rules
  • Correct use of sync.Mutex: receiver types, copying, and unlocking on every path
  • Which paths could still bypass the lock, and how to close them

Hints

Hint 1

Interleave two calls: Write out two concurrent deposits one step per line, and note what each thread reads before the other one writes.

Hint 2

Where the lock lives: Decide which object should own the synchronization state, when it should be created, and how many accounts it should serialize.

Hint 3

Follow each call: For every method a caller can invoke on your new type, trace which implementation actually runs and whether that path goes through your synchronization.

Follow-up questions

  • Add `transfer(source, target, amount)` between two thread-safe accounts. How do you avoid a deadlock when two transfers run in opposite directions at the same time?
  • A teammate argues that once the sleep is removed, the GIL makes `self.balance += amount` atomic, so no lock is needed. How do you respond?
  • A new method `deposit_many(amounts)` on your safe class takes the lock and then calls `self.deposit` in a loop. What happens with `threading.Lock`, and what would you change?
  • The balance moves into a database that several service instances update. Which of your techniques still apply, and what replaces the in-process lock?