Excellent question.
This shows you’re thinking at the exact point where many engineers level up.Short answer first, then the real answer:
NO — you don’t always have to run a direct UPDATE.
YES — you must ensure the read–check–write is ATOMIC.There are two correct ways to avoid unsafe “read shared data”.
Let’s understand when to use which.1️⃣ The Real Problem (Re-stated)
The dangerous pattern is:
value = read(); if (condition based on value) { write(); }Because:
- Another transaction can modify value between read and write.
So your goal is:
Prevent other transactions from changing the data between read and write.
2️⃣ Correct Option #1 — Atomic UPDATE (Best & Simplest)
✅ Database-Driven Safety (Highly Recommended)
UPDATE account
SET balance = balance - 70
WHERE id = 1 AND balance >= 70;
What happens:
- DB reads balance
- DB checks condition
- DB updates ? All in ONE atomic operation
In Spring Boot:
@Modifying
@Query("""
UPDATE Account a
SET a.balance = a.balance - :amount
WHERE a.id = :id AND a.balance >= :amount
""")
int withdraw(@Param("id") Long id, @Param("amount") int amount);
@Transactional
public void withdraw(Long id, int amount) {
int updated = repo.withdraw(id, amount);
if (updated == 0) {
throw new InsufficientFundsException();
}
}
✔ No race condition
✔ No locking complexity
✔ Very scalable
? Banks LOVE this pattern
3️⃣ Correct Option #2 — Read + Lock + Update (Also Safe)
Sometimes you must read the data (business logic, validations).
✅ Pessimistic Locking
@Transactional
public void withdraw(Long id, int amount) {
Account acc = repo.findByIdForUpdate(id); // LOCK
if (acc.getBalance() < amount)
throw new InsufficientFundsException();
acc.setBalance(acc.getBalance() - amount);
}
DB behavior:
- Row locked at read
- No other transaction can change it
- Safe read-check-write
✔ Safe
❌ Less scalable (threads wait)
4️⃣ Which One Should YOU Use?
? Use Atomic UPDATE when:
- Simple condition
- No complex business logic
- High concurrency
- Counters, balances, inventory
? Use Read + Lock when:
- Complex validations
- Multiple fields involved
- Business rules depend on current state
5️⃣ What You Should NOT Do ❌
Account acc = repo.findById(id); // NO LOCK
if (acc.getBalance() >= amount) {
acc.setBalance(acc.getBalance() - amount);
repo.save(acc);
}
This will break under load.
6️⃣ Important Subtle Point (Very Senior-Level)
ORM makes unsafe patterns look safe.
JPA encourages:
find → modify → save
But DB safety does NOT come automatically.
? You must force safety via:
- Locking
- Atomic queries
- Versioning
7️⃣ One-Sentence Rule (MEMORIZE)
If you read shared data to decide a write, the DB must prevent concurrent modification.
8️⃣ Quick Decision Table
| Scenario | Best Approach |
|---|---|
| Balance / stock | Atomic UPDATE |
| Order state machine | Conditional UPDATE |
| Complex validation | Pessimistic lock |
| High read, low write | Optimistic lock |