Sitemap

Implementing Optimistic Locking in Your Applications: A Comprehensive Guide

5 min readJan 16, 2025

--

Press enter or click to view image in full size

Concurrency is a critical concern in modern application development. When multiple users or processes access and modify shared resources simultaneously, ensuring data consistency can be challenging. Optimistic Locking is a powerful technique to address this challenge without compromising performance.

In this blog, we’ll explore what optimistic locking is, how it works, and how you can implement it effectively. We’ll also cover potential pitfalls and best practices to make you a pro at handling concurrency in your applications.

What is Optimistic Locking?

Optimistic locking is a concurrency control mechanism that assumes multiple transactions can frequently complete without conflicting with each other. Instead of locking resources during an operation, optimistic locking allows all operations to proceed. Conflicts are detected and resolved only at the point of committing the transaction.

Get Anudeep Balla’s stories in your inbox

Join Medium for free to get updates from this writer.

This strategy is particularly useful in scenarios where:

  • Data conflicts are rare.
  • High read and write performance is required.

If a conflict occurs, the transaction is retried or aborted based on your application’s logic.

Key Concepts Behind Optimistic Locking

To understand optimistic locking, let’s break it down into three simple concepts:

  1. Versioning: Each record in the database includes a version field (e.g., a number or timestamp) that tracks updates to the record.
  2. Concurrency Check: When a transaction tries to update a record, it checks if the version in the database matches the version it retrieved.
  3. Conflict Handling: If the versions don’t match, it means another transaction has modified the record. The current transaction can then retry or fail gracefully.

How Optimistic Locking Works

Let’s walk through the process:

  1. Fetch the Record: Retrieve the data from the database, including its version.
  2. Perform Local Updates: Modify the data in your application code.
  3. Attempt Update: When committing changes back to the database, include a conditional check on the version field.
  4. Conflict Resolution: If the version in the database doesn’t match the expected version, the database rejects the update, indicating a conflict.
  5. Retry Logic: Based on your logic, you can either retry the transaction or abort it with an error.

Why Use Optimistic Locking?

Optimistic locking offers several benefits:

  1. High Performance: Since it doesn’t require locking resources during operations, it minimizes system bottlenecks.
  2. Scalability: Ideal for systems with high read-to-write ratios, such as e-commerce applications.
  3. Conflict Detection: Ensures data consistency without affecting performance.

However, it’s not suitable for scenarios with frequent conflicts or high contention, where pessimistic locking may be a better fit.

Step-by-Step Guide to Implementing Optimistic Locking

Let’s implement optimistic locking in a Java application using JPA (Java Persistence API) and a relational database.

1. Define the Database Table

Here’s a table schema for an example Product entity:

CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
version INT NOT NULL
);

The version column is crucial for optimistic locking.

2. Create the Entity Class

In JPA, the @Version annotation marks the field used for optimistic locking. Add it to your entity class as follows:

import jakarta.persistence.*;

@Entity
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private double price;

@Version
private int version;

// Getters and setters
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public int getVersion() {
return version;
}

public void setVersion(int version) {
this.version = version;
}
}

3. Update the Record

When updating a record, JPA automatically uses the version field to ensure no conflicts occur. Here’s a simple example:

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class ProductService {

private EntityManagerFactory emf = Persistence.createEntityManagerFactory("productPU");

public void updateProductPrice(Long productId, double newPrice) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

try {
Product product = em.find(Product.class, productId);
product.setPrice(newPrice);

// Persist changes
em.getTransaction().commit();
} catch (Exception e) {
em.getTransaction().rollback();
System.out.println("Update failed due to a conflict: " + e.getMessage());
} finally {
em.close();
}
}
}

In this example, if the version in the database doesn’t match the version in memory, JPA throws an OptimisticLockException.

4. Handle Conflicts

To handle conflicts, implement retry logic. Here’s an example:

public void safeUpdateProductPrice(Long productId, double newPrice) {
boolean success = false;
int retries = 3;

while (!success && retries > 0) {
try {
updateProductPrice(productId, newPrice);
success = true;
} catch (OptimisticLockException e) {
retries--;
System.out.println("Retrying update... Attempts left: " + retries);
}
}

if (!success) {
throw new RuntimeException("Failed to update product after multiple attempts.");
}
}

5. Test Your Implementation

Write unit tests to verify optimistic locking behavior. Simulate concurrent transactions to ensure the application detects and handles conflicts correctly.

Optimistic Locking in Other Contexts

Spring Data JPA

If you’re using Spring Data JPA, optimistic locking works seamlessly with the @Version annotation. Handle OptimisticLockingFailureException in your service layer to retry or abort transactions.

Manual SQL Implementation

If you’re not using JPA, you can implement optimistic locking directly in SQL:

UPDATE product
SET price = 20.99, version = version + 1
WHERE id = 1 AND version = 2;

Here, the WHERE clause ensures the update succeeds only if the version matches.

NoSQL Databases

In NoSQL databases like MongoDB, you can use a similar approach by including a version or timestamp field and performing conditional updates.

Best Practices

  1. Use Where Necessary: Optimistic locking is ideal when conflicts are rare. For high-contention scenarios, consider pessimistic locking.
  2. Log Conflicts: Maintain logs for failed transactions to identify patterns and optimize accordingly.
  3. Educate Your Team: Ensure all developers understand how optimistic locking works and its implications.
  4. Design Retry Logic: Implement robust retry mechanisms to handle conflicts gracefully.

Optimistic locking is a highly effective strategy for managing concurrency in modern applications. By leveraging version control and conflict detection, you can ensure data consistency without sacrificing performance. Whether you’re using JPA, manual SQL, or NoSQL, mastering this technique will significantly enhance your ability to build scalable and reliable systems.

Dive into your projects and start implementing optimistic locking today! Have questions or need more examples? Drop them in the comments below. Happy coding!

--

--