home..

Optimistic Locking

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:

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

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:


  1. FModel can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS. 

© 2024 fraktalio d.o.o.