Transaction isolation controls which concurrent database changes a transaction may observe and which anomalies its database prevents. Spring supplies an isolation hint to the transaction manager; the database implements the actual behavior.
Spring transaction isolation: state the anomaly you need to prevent
Choose the invariant before the annotation
For a stock reservation, the invariant is that committed reserved quantity must not exceed available stock. An annotation alone cannot express that rule. A conditional update with an affected-row check, a version column, or a database constraint may be clearer than repeatedly reading and writing a value under an assumed isolation level.
package in.aitrove.receipts;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
class StockReservation {
private final JdbcTemplate jdbc;
StockReservation(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean reserve(long stockId, int units) {
if (units <= 0) throw new IllegalArgumentException("units must be positive");
int changed = jdbc.update(
"UPDATE stock SET available = available - ? " +
"WHERE id = ? AND available >= ?",
units, stockId, units);
return changed == 1;
}
}The SQL predicate makes the check and decrement one database statement. It avoids a separate read-then-write race, provided the database executes the statement atomically. Add a database test with two concurrent transactions and the production database engine. H2 behavior is not enough evidence for another engine.
Know when the isolation hint is ignored
With REQUIRED propagation, a method called inside an existing transaction joins the outer transaction by default. Its local isolation setting may not replace the outer setting. A self-call that bypasses the Spring proxy may not open the expected boundary at all. Higher isolation can increase blocking or aborts; retry only a command whose external effects are safe to repeat.
Common Mistakes
- Claiming SERIALIZABLE is always the fastest or safest application-wide default.
- Expecting a joined inner method to change an already-started transaction.
- Retrying the whole method after a serialization failure when it also sent an external message.
- Testing only sequential reservations.
