Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring transaction isolation: state the anomaly you need to prevent

Last updated: 1 Oct 20265 min read
tutorial
IntermediateBy AITrove Editorial

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.

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.

Java
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.

Read next

Propagation, optimistic locking, and versioned JDBC writes.

spring
spring-boot
transaction-isolation-choice
Storage details