Make a Racy BankAccount Deposit Thread-Safe Without Editing the Class, in Python and Go
xAISoftware Engineer · OnsiteLast 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
BankAccountitself. Copying its body into a new class and editing the copy is also rejected: the thread-safe version must reuse the existingdepositthrough 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
amountbe 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?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.