Optimistic Locking
Written by Ivan Dugalic - June 2023
Concurrency control is the process of managing and coordinating concurrent access to shared resources in a multi-user environment. In the context of databases and distributed systems, concurrency control aims to ensure data consistency while allowing multiple transactions to execute concurrently. This is particularly challenging when different transactions access and modify the same data simultaneously.
Traditionally, concurrency control mechanisms utilize locking techniques to prevent conflicts and maintain data integrity. Locks are used to establish exclusive access to a shared resource, ensuring that only one transaction can modify it at a time. While this approach guarantees data consistency, it can lead to reduced performance and increased contention when multiple transactions attempt to access the same resource simultaneously.
Introducing Optimistic Locking
Optimistic locking presents an alternative approach to concurrency control that allows concurrent access to shared resources, thus improving system performance. The key idea behind optimistic locking is to assume that conflicts among transactions are rare, and therefore, they should be validated only when necessary. This approach reduces the need for acquiring and releasing locks, minimizing contention and improving scalability.
In optimistic locking, each transaction is associated with a version or timestamp that represents the state of the data at the beginning of the transaction. When a transaction wants to modify a shared resource, it first checks whether the resource’s version matches its own. If the versions match, the transaction proceeds with the modification. However, if the versions do not match, it means that another transaction has modified the resource in the meantime, resulting in a conflict. In such cases, the conflicting transaction is rolled back, and the current transaction can be retried or an appropriate resolution strategy can be applied.
Understanding Event Sourcing
Event sourcing is a pattern where an application’s state is derived by applying a series of events to an initial state. Instead of storing the current state directly, event sourcing stores a log of events that have occurred in the system. These events represent the changes that have been made and can be replayed to reconstruct the application’s state at any given point in time. This approach provides a historical record of all actions and allows for easy traceability and auditing.
Optimistic Locking in Event Sourcing
Optimistic locking can be effectively applied in event sourcing to handle concurrent modifications to the system’s state. The goal is to allow multiple commands or transactions to be processed concurrently while ensuring consistency and preventing conflicts.
In event sourcing, each aggregate or entity has a corresponding event stream that captures all events related to that entity. Optimistic locking in event sourcing relies on versioning the events within the event stream. The version number represents the sequence or order of events that have occurred for a specific entity.
When a command or transaction is executed, the system checks the current version of the entity’s event stream. If the version matches the expected version, the command is allowed to proceed, and the resulting events are appended to the stream. However, if the version does not match, it indicates that another command has modified the entity’s state in the meantime, leading to a conflict.
Handling Conflicts
When a conflict is detected due to a version mismatch, appropriate conflict resolution strategies need to be applied. Here are some common approaches:
- Retrying the Transaction: One straightforward approach is to retry the transaction after updating the expected version to the latest version. This allows the conflicting transaction to complete and ensures that subsequent transactions are executed on the most recent state.
- Merging Conflicting Changes: In some cases, conflicts can be resolved by merging the conflicting changes. This approach requires analyzing the nature of the conflict and applying specific merge strategies to reconcile the differences.
- Resolving with Business Rules: Depending on the application’s requirements, conflicts can be resolved by applying predefined business rules or policies. For example, conflicting changes in a shopping cart may be resolved by considering the most recent change or favoring the customer’s preferences.
Implementing Optimistic Locking in Event Sourcing
Your domain model/core logic should not be aware of the underlying concurrency control mechanism. It should be implemented as a cross-cutting concern within the infrastructure/adapter layer by versioning the events within the event stream. The version number represents the sequence or order of events that have occurred for a specific entity.
‘FModel library’1 provides a simple way to implement optimistic locking in event sourcing by providing dedicated interface(s):
interface EventSourcingLockingAggregate<C, S, E, V> : EventComputation<C, S, E>, EventLockingRepository<C, E, V>
Observe that EventSourcingLockingAggregate
extends EventComputation
and EventLockingRepository
interfaces, where
EventComputation
represents domain model/pure core logic/command handling algorithm,- and
EventLockingRepository
represents infrastructure layer, responsible for fetching and storing events and its version numbers.
interface EventLockingRepository<C, E, V> {
fun C.fetchEvents(): Flow<Pair<E, V>>
fun Flow<E>.save(latestVersion: V?): Flow<Pair<E, V>>
}
C
represents Command. E
represents Event. Version number is represented by V
type parameter, and it is only present in EventLockingRepository
interface, not in EventComputation
interface.
To make it more obvious, let’s take a look at the regular EventSourcingAggregate
interface, that does not support optimistic locking:
interface EventSourcingAggregate<C, S, E> : EventComputation<C, S, E>, EventRepository<C, E>
interface EventRepository<C, E> {
fun C.fetchEvents(): Flow<E>
fun Flow<E>.save(): Flow<E>
}
Observe that EventSourcingAggregate
and EventSourcingLockingAggregate
interfaces are identical, except that EventSourcingLockingAggregate
interface extends EventLockingRepository
interface, and EventSourcingAggregate
interface extends EventRepository
interface.
You can compose your application using one/unique domain model/core logic, and multiple infrastructure implementations, depending on your needs. The implementation of repositories is on you, and it can be done using any technology you like, as long as you implement the required interfaces.
Ordering of Events, commutativity and optimistic locking
When operations are commutative, concurrent execution of those operations will not result in conflicts or inconsistencies. This is because the order in which the operations are executed does not affect the final state of the system. In such scenarios, optimistic locking may not be necessary as conflicts are unlikely to occur.
For example, consider a simple bank account system with commutative operations such as deposits and withdrawals. If two concurrent transactions attempt to deposit $100 and withdraw $50 respectively, the order of executing these operations does not affect the final balance. Whether the deposit occurs before the withdrawal or vice versa, the result will be the same, i.e., an increase of $50 in the account balance.
In cases where operations are commutative, the system can rely on the inherent property of commutativity to handle concurrency control without explicitly using optimistic locking. This can lead to improved performance and scalability as there is no need for validation or conflict resolution.
However, it’s important to note that not all operations in a system are inherently commutative. Many real-world scenarios involve non-commutative operations where the order of execution does matter. In such cases, optimistic locking or other concurrency control mechanisms are still required to ensure data consistency.
More on that in some of the next articles.
Example
Check out the demo application(s), where you can find an example of optimistic locking in event sourcing, implemented using FModel library: