In Spring, the @Transactional annotation is used to define transactional behavior for a method or class. One important aspect of transactions is isolation level, which determines how one transaction is isolated from others. Isolation levels help prevent data anomalies like dirty reads, non-repeatable reads, and phantom reads.
1. Understanding Isolation Levels
A transaction isolation level defines how data changes made by one transaction are visible to other concurrent transactions. Databases support different isolation levels; Spring allows you to configure them via @Transactional(isolation = ...).
1.1 Key Data Anomalies
- Dirty Read
- Occurs when a transaction reads uncommitted data from another transaction.
- Example: Transaction A updates a row but hasn’t committed yet. Transaction B reads that row. If A rolls back, B read invalid data.
- Non-Repeatable Read
- Occurs when a transaction reads the same row twice but gets different values because another transaction modified it in between.
- Example: Transaction A reads a value. Transaction B updates it and commits. Transaction A reads again and sees a different value.
- Phantom Read
- Occurs when a transaction reads a set of rows twice but gets different results due to insertion or deletion by another transaction.
- Example: Transaction A queries for all orders with amount > 100. Transaction B inserts a new qualifying order. Transaction A re-queries and sees the new row.
2. Isolation Levels in Spring
Spring’s @Transactional supports all standard JDBC isolation levels via the Isolation enum:
| Isolation Level | Description | Prevents | Default Behavior |
|---|---|---|---|
DEFAULT | Uses the default isolation level of the database. | Depends on DB (usually READ_COMMITTED) | Depends on DB |
READ_UNCOMMITTED | Allows reading uncommitted changes from other transactions. | None | May cause dirty reads |
READ_COMMITTED | Only committed data can be read. | Dirty reads | Standard default for most DBs |
REPEATABLE_READ | Same row read multiple times returns the same value. | Dirty reads, non-repeatable reads | PostgreSQL default |
SERIALIZABLE | Full isolation; transactions execute sequentially. | Dirty reads, non-repeatable reads, phantom reads | Highest isolation, lowest concurrency |
2.1 READ_UNCOMMITTED
- Lowest isolation level.
- Allows dirty reads.
- Rarely used in production.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
// Read data even if other transactions haven't committed
}
Example Scenario: Transaction B reads a value updated by Transaction A before A commits. If A rolls back, B has read invalid data.
2.2 READ_COMMITTED
- Most commonly used isolation level.
- Prevents dirty reads.
- Allows non-repeatable reads and phantom reads.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
// Only committed data is visible
}
Example Scenario: Transaction B can read a row updated by Transaction A only after A commits.
2.3 REPEATABLE_READ
- Prevents dirty reads and non-repeatable reads.
- Phantom reads may still occur (depends on DB implementation).
- Ensures multiple reads of the same row in a transaction return the same value.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() {
// Read same row multiple times; values are consistent
}
Example Scenario: Transaction A reads a row twice. Transaction B tries to update the same row. Transaction A sees the same value both times.
2.4 SERIALIZABLE
- Highest isolation level.
- Prevents dirty reads, non-repeatable reads, and phantom reads.
- Transactions are executed as if sequentially.
- Can cause performance overhead due to locking.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
// Full isolation
}
Example Scenario: Transaction A reads a set of rows. Transaction B cannot insert/update rows that would affect Transaction A until it completes.
2.5 DEFAULT
- Uses database default isolation level.
- Good if you want Spring to delegate to the DB.
@Transactional(isolation = Isolation.DEFAULT)
public void defaultIsolationExample() {
// DB default applies
}
3. Practical Example
Suppose you have an account transfer service:
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transfer(Long fromId, Long toId, Double amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
accountRepository.save(from);
accountRepository.save(to);
}
}
- Using
REPEATABLE_READensures that during the transaction, the balances read will not change due to other concurrent transfers. - If
READ_COMMITTEDis used, another transaction could modify balances between reads, causing inconsistencies.
4. Choosing the Right Isolation Level
| Requirement | Recommended Isolation Level |
|---|---|
| Prevent dirty reads only | READ_COMMITTED |
| Prevent dirty & non-repeatable reads | REPEATABLE_READ |
| Full data consistency, prevent phantoms | SERIALIZABLE |
| Maximum performance, minimal locking | READ_UNCOMMITTED |
Best Practice: Start with READ_COMMITTED (default for most DBs), increase isolation only if your use case demands stricter consistency.
