Make a Racy BankAccount Deposit Thread-Safe Without Editing the Class, in Python and Go
Company: xAI
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Onsite
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:
```python
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.
```hint Interleave two calls
Write out two concurrent deposits one step per line, and note what each thread reads before the other one writes.
```
#### 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.
```hint Where the lock lives
Decide which object should own the synchronization state, when it should be created, and how many accounts it should serialize.
```
#### 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:
```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.
```hint 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.
```
#### 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
### What a Strong Answer Covers
- A diagnosis that separates the root cause from the symptom amplifier
- A fix that satisfies the no-edit, no-copy constraint in both languages
- The cost of holding a lock across a slow call, and re-entrancy (`Lock` versus `RLock`)
- Tests that expose the lost update deterministically instead of relying on luck
### 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?
Overview: 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.