Revisiting Optimistic and Pessimistic Locking in the Age of Saga and Outbox
Recently, I’ve been spending quite a bit of time studying distributed transaction patterns such as Saga, Transactional Outbox, and…
Revisiting Optimistic and Pessimistic Locking in the Age of Saga and Outbox
Recently, I’ve been spending quite a bit of time studying distributed transaction patterns such as Saga, Transactional Outbox, and event-driven architectures. In my current project, we are gradually migrating to an EDA (Event-Driven Architecture), progressively upgrading services and implementing the Saga and Outbox patterns to achieve reliable distributed consistency.
During this process, I realized that some problems I had previously overlooked at the local transaction level can still have a big impact: how to handle concurrent updates safely within a single service, prevent lost updates, and maintain correctness before events leave the service boundary.
This observation naturally led me to revisit two classic concurrency control mechanisms: optimistic locking and pessimistic locking, which remain highly relevant even in modern cloud-native systems.
In this article, I will review how these locks work in Spring Boot JPA and how to implement them using
@Transactional, and in the case of optimistic locking, how to leverage a version field in the database along with the@Versionannotation in the entity to automatically detect conflicting updates. I will also cover pessimistic locking, which locks the database row immediately during a transaction, and discuss best practices for both approaches in modern cloud-native architectures.

Optimistic vs Pessimistic Locking
In Spring JPA and relational databases, optimistic and pessimistic locking are two common strategies to handle concurrent access to the same data. Each approach has its assumptions, mechanisms, and trade-offs. Below is a detailed comparison.
Assumptions
- Optimistic Lock: Assume that concurrent conflicts are rare. Multiple transactions can read and update the same data without blocking, and conflicts are detected only at commit time.
- Pessimistic Lock: Assume that concurrent conflicts are likely. Transactions lock the data immediately to prevent other transactions from modifying it until the concurrent transaction completes.
Database Support

- Optimistic Lock: Requires a version column or timestamp in the database table. JPA maps this to an entity field annotated with
@Version. Hibernate automatically checks the version when updating and raises an exception if the version has changed. - Pessimistic Lock: Use row-level locks, typically via SQL statements like
SELECT ... FOR UPDATE. The database ensures that no other transactions can modify the locked rows until the transaction commits or rolls back.
Mechanism
- Optimistic Lock: Checks the version field at commit. If the version in the database differs from the version in memory, the transaction fails with an
OptimisticLockException. This allows high read concurrency but requires retry logic for conflicts. - Pessimistic Lock: Locks the row immediately at the start of the transaction. Other transactions attempting to read/write the same row for update are blocked until the lock is released. This ensures strong consistency, but may reduce throughput in high-contention scenarios.
Performance Considerations
- Optimistic Lock: Performs well for high-read, low-write workloads, since it avoids unnecessary database locks and allows more concurrent transactions.
- Pessimistic Lock: Better suited for high-contention, critical resources, where lost updates or inconsistent data cannot be tolerated. Locks may cause transactions to wait, so throughput can be lower.
Sprint Boot / JPA Support
- Optimistic Lock: Implemented via the
@Versionannotation in entities. Example:
@Entity
public class Account {
@Id
@GeneratedValue
private Long id;
private double balance;
@Version
private Long version; // used for optimistic locking
}
- Pessimistic Lock: Implemented via the
@Lockannotation in Spring Data JPA repositories:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Account a where a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);
Optimistic Locking in Spring Boot JPA
Setup: Versioned Entity
import jakarta.persistence.*;
@Entity
public class Account {
@Id
@GenertedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String owner;
private double balance;
@Version
private Long version; // used by JPA for optimistic lock
}
@Versiontell JPA / Hibernate to track a version field.- Every
updatechecks the version. If another transaction updated first,OptimisticLockExceptionis thrown.
Service Method with @Transactional
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
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);
}
}
- If there are two transactions, try to transfer money from the same account at the same time; one will fail with
OptimisticLockException - We can retry the transaction in our service when this exception occurs.
Pessimistic Locking in Spring Boot JPA
Using @Lock annotation
import org.springframework.data.jpa.repository.*;
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Account a where a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);
}
Service Method
@Transactional
public void transferWithPessimisticLock(Long fromId, Long toId, double amount) {
Account from = accountRepository.findByIdForUpdate(fromId);
Account to = accountRepository.findByIdForUpdate(toId);
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
accountRepository.save(from);
accountRepository.save(to);
}
- Row is locked immediately; other transactions trying to read for write will wait until commit.
- Prevents conflicts but may reduce concurrency.
Best Practices in Cloud-Native Environments
Optimistic Locking in Cloud-Native
In modern cloud-native systems, optimistic locking is often the preferred choice. Most microservice-based applications are designed to scale horizontally, and database contention is typically kept relatively low through service decomposition and workload distribution.
Optimistic locking works particularly well for scenarios where concurrent conflicts are infrequent, such as:
- User profile updates
- Product catalog management
- Configuration changes
- Business entities that are read frequently but updated occasionally
By avoiding database-level locks, optimistic locking allows multiple transactions to proceed concurrently and only detects conflicts during the update phase. This significantly reduces database contention and improves overall system throughput.
When implementing optimistic locking with Spring Boot and JPA, it is recommended to:
- Use a dedicated version field annotated with
@Version - Handle
OptimisticLockExceptiongracefully - Implement retry mechanisms with exponential backoff when appropriate
- Keep transactions short and focused on a single business operation
For these reasons, optimistic locking is commonly considered the default choice for cloud-native applications.
Pessimistic Locking in Cloud Native
Pessimistic locking takes the opposite approach by assuming that concurrent conflicts are likely to occur. Instead of detecting conflicts after the fact, it prevents them by acquiring a database lock as soon as the transaction begins.
This strategy is most appropriate for business operations involving highly contested resources, where data correctness is more important than throughput.
Typical examples include:
- Inventory reservation systems
- Payment processing
- Financial account operations
- Ticket booking platforms
- Limited-stock flash sales
In these scenarios, allowing concurrent updates may result in overselling, duplicate processing, or inconsistent balances. A pessimistic lock ensures that only one transaction can modify the protected record at a time.
However, pessimistic locking comes with trade-offs. Long-held database locks can reduce throughput, increase latency, and potentially lead to deadlocks under heavy load.
To minimize these risks, several best practices should be followed:
- Use pessimistic locking only for critical sections of the application
- Keep transactions as short as possible
- Avoid external API calls while holding database locks
- Configure lock wait timeouts at the database level
- Monitor lock contention and deadlock metrics in production
Because of this impact on scalability, pessimistic locking should generally be applied selectively rather than as a default strategy.
Choosing Between the Two
As a general rule, modern cloud-native architectures tend to favor optimistic locking because it aligns better with horizontal scalability and distributed system design principles.
Pessimistic locking remains valuable for a small number of highly critical business operations where conflicts are frequent, and the cost of inconsistency is unacceptable.
A useful mental model is:
Use optimistic locking when conflicts are rare and scalability is important; and use pessimistic locking when conflicts are common and correctness is paramount.
Even in systems that adopt advanced patterns such as Saga, Transactional Outbox, or Event-Driven Architecture, local concurrency control remains an important consideration. Distributed consistency patterns solve coordination problems between services, but optimistic and pessimistic locking continue to play a crucial role in protecting data integrity within individual service boundaries.
Practical Recommendations
- Optimistic Lock: Default choice for microservices / cloud-native applications.
- Pessimistic Lock: Use selectively for highly contexted resources.
- Combine: Use optimistic locking in most cases, but switch to pessimistic in rare critical sections.
- Keep transactions short: Cloud-native DBs scale better with short, fast transactions.
- Monitoring & Metrics: Track
OptimisticLockExceptionrate → this helps tune retry logic.
메타데이터
- post_id
- ffaf72ec034b
- slug
- revisiting-optimistic-and-pessimistic-locking-in-the-age-of-saga-and-outbox-ffaf72ec034b
- url
- https://medium.com/@rurutia1027/revisiting-optimistic-and-pessimistic-locking-in-the-age-of-saga-and-outbox-ffaf72ec034b
- canonical_url
- https://medium.com/@rurutia1027/revisiting-optimistic-and-pessimistic-locking-in-the-age-of-saga-and-outbox-ffaf72ec034b
- author_url
- https://medium.com/@rurutia1027
- status
- ok
- fetched_at
- 2026-06-29 01:02:39