Optimistic locking in practice

iamprovidence
8 min readMar 17, 2023

--

Soooo, you are building an app. You have tested it. Everything works like a charm. You have deployed it. Now multiple people start using it simultaneously and boom 💥 we are pucked 🤦‍♂️ nothing works 😔

You see, when you are developing an application, you tend to think of it as a single user using it. After all, you are a single person. It is hard to develop it in a way sustainable for parallel usage. Hard, but possible.

Mastering such skills will help you to do another step to become a better developer. And you know what? This is your lucky day. That is exactly what this article is about.

In this story, you will see why simple CRUD is more complicated than you think. The problems can occur when multiple users use your app in parallel. How those can be fixed? And is efforts cost your time.

This is just the beginning of a series. Once you’ve completed this one, be sure to explore the subsequent story:

If you are ready, let’s begin.

We will start with something simple. Imagine you are working at an online store 😄 Your task is to implement a reliable admin panel where administrators would be able to list all products, add new, update, and delete them. That’s how simple, it is.

In our system, we have only two admins — Alice and Bob. One day, Alice decided to rename the product, because she just figured out a new funky name. What a coincidence! At the same moment, Bob decided to rename exactly the same record, too.

So, here we are. Alice and Bob, are just two regular users who doing the same thing in parallel. Let’s observe them, and see where it gets.

  1. Alice opens the record at number 19 and oops, a teapot starts getting crazy. Alice leaves the page open, and takes a small coffee break.☕
  2. Meanwhile, Bob opens the same record. Rename it. And go for a walk. 🚶
  3. When Alice comes back, she enters the name she likes and presses the big shine save button.
  4. Imagine the surprised face of Bob when he returns and realizes the saved name is not the one he entered 😆. He asks Alice, but the poor girl claims she has not seen the name Bob entered.

So who is right here? 🤔

  • Is it Alice, because she was the last one pressing the save button?
  • Or maybe it is Bob, since he was the first one to save the data?
  • Or is it still Alice, because she was the first who started editing the record?

We can not really help Alice and Bob to settle the dispute. But we can prevent it from happening again.

Our goal will be to show users they are updating outdated data.

Alright, we have a bug in our system. We open the ticket, annoyingly. Read the description, distrustfully. Users are confused, yeah yeah yeah, blah blah blah, I got you 😒

But wait! We as developers are confused as well. Shouldn’t transaction isolation levels help us in this case?🤔 Why the hell, I have spent so much time learning those and answering questions about them on every pucking job interview.

Yeaaaah… not really. I am sorry to tell you buddy it is not that simple.😔

They help. They surely do help. But, you see, often a business transaction executes across multiple db transactions.

Let’s check the user story description once again:

Even though, it seems like one action, it’s just impossible to handle it with one endpoint. You will always need one to retrieve the data and one to update it. And those two endpoints will have their own transactions. Check it out:

[ApiController]
[Route("products")]
public class ProductController : ControllerBase
{
private readonly StoreDbContex _dbContext;

public ProductController(StoreDbContex dbContext)
{
_dbContext = _dbContext;
}

[HttpGet]
public IActionResult GetAll()
{
// even when you don't specify transaction explicitly
// EF will do it for you
// here it is done only for educational purpose
// so you could see system transaction's border more clearly
using (var transaction = context.Database.BeginTransaction())
{
returns _dbContext.Products.ToList()
}
}

[HttpPost]
public IActionResult Update(Product product)
{
using (var transaction = context.Database.BeginTransaction())
{
_dbContext.Products.Update(product);
_dbContext.SaveChanges();
}
}
}

We can not depend on our database transactions anymore to ensure that data will be in a consistent state.

So what do we do then? We implement transaction mechanism by ourselves! Lucky we are, there is no need to reinvent the wheel. Smart people already did everything for us and called it optimistic locking. All we need to do is learn their legacy 😌 The key here is to version our records.

Let’s see how it works:

  • Alice gets the record №19 with the version of 1.
  • Bob gets the same record №19 with the version of 1.
  • Both Alice and Bob modify the data locally.
  • Bob wants to save his data. We validate whenever the current version in DB is the same, which it is, so it can be saved. The version of the record now increased to 2.
  • Now Alice wants to save her data. We validate whenever the current version in DB is the same, and nope, they are different. In that case, the action is aborted and Alice should start over again. By doing so, she will see the name Bob entered, so can decide whenever her name is better.

The idea is pretty intuitive. Now that we have seen it in the theory, let’s try it in the practice:

Firstly, we need to add a version column to our model. It does not necessarily have to be a number. We can also use a timestamp, a hash, and so on.

public class Product
{
public int Id { get; set; }
public string Name { get; set; }

// version column
public DateTime UpdatedAt { get; set; }
}

When it comes to implementing a locking itself it is just a matter of a few lines. Get the data. Check the version. Abort if it’s wrong. Increase and save if it’s correct.

[ApiController]
[Route("products")]
public class ProductController : ControllerBase
{
. . .

[HttpPost]
public IActionResult Update(Product product)
{
var productFromDb = _dbContext.Products.Find(product.Id);

// check the version is the same
if (productFromDb.UpdatedAt != product.UpdatedAt)
{
throw new ConcurrencyException(@"
Nope buddy. You were too late.
Refresh the page ¯\_(ツ)_/¯");
}

// update the version
product.UpdatedAt = DateTime.Now;

_dbContext.Products.Update(product);
_dbContext.SaveChanges();
}
}

That’s it! It was easier than you thought, wasn’t it? 😏

As you can see, optimistic locking solves the problem of parallel usage, by validating that the changes about to be committed by one session don’t conflict with the changes of another session.

For those of you, who have reached that far, I have a bonus section 🎁

If you are a fan of Entity Framework, you will be glad to hear it does support optimistic locking from the box 😱

It can be achieved with both attributes and FluentAPI. For example, the following attributes are supported:

  • [ConcurencyCheck] — using it, the developer is responsible for managing and updating the version of the record
  • [Timestamp] — in this case, the Database is responsible for managing the version and automatically changing it when the record is updated

Both approaches fix the initial issue. Which one to choose is up to you, just make sure you are consistent across the entire application.

public class Product
{
public int Id { get; set; }
public string Name { get; set; }

// Developer update version mannualy
[ConcurrencyCheck]
public int RevisionNumber { get; set; }

// DB update verion automatically
// [Timestamp]
// public byte[] Version { get; set; }
}

Here is a small console example of how it can be used:

var product = GetFirstProduct();
product.Name = "Updated";
UpdateProduct(product);


Product GetFirstProduct()
{
using (StoreDbContex db = new StoreDbContex())
{
return db.Products.First();
}
}

void UpdateProduct(Product product)
{
using (StoreDbContex db = new StoreDbContex())
{
// if your local version and value in db does not match
// you got a DbUpdateConcurrencyException

// product.RevisionNumber += 1;

db.Products.Update(product);
product.RevisionNumber += 1;

try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// handle concurency exception
}
}
}

The EF team approached the versioning issue a bit extraordinarily. To minimize the number of queries, instead of loading the record beforehand, they check the version during the update operation and compare how many records were updated to the expected value:

UPDATE [Products]
SET [Name] = 'Updated', [Version] = [Version] + 1
WHERE [Id] = 1 AND [Version] = 1

You can get some inspiration from them. There is no real restriction on how it can be done. You can also find a record by Id and Version and then check if it is null or not. You can come up with something yours. Just make sure it works, and let me know 😅

Wrapping it up

Optimistic locking prevents conflicts between business transactions by detecting a conflict and aborting the transaction.

  • It’s called optimistic because we are optimistic here and assume that the chance of conflict is very low.
  • It fits best when there are many records and relatively few users. An example of such a case would be an Admin panel.
  • It allows multiple users to work on the same data. (at least read the same data 😅)
  • It costs less to do an operation since there is no locking and concurrency issue.
  • However, when a collision does occur, your users are doomed. They have to restart an action from the very beginning. It means not only an unpleasant user experience but also a performance downgrade.

That is basically all you need to know, about optimistic locking, to start applying it in your program 😉.

Get ready, because after summarizing everything, I’m going to say something controversial. You know, sometimes we don’t need a locking mechanism at all 😃. I have seen numerous apps that require locking but work without it just fine. It may be your case as well.

Although, it does not mean you should not learn it. At least now, when you face the monster, you know how to deal with it 😉.

That’s all for today, folks 🐷

I hope you enjoyed it and learned something new 📑

Don’t forget to clap, so I know you are interested 🎬

Follow to get more articles on concurrency ✅

You can also support me if you want to continue seeing those ☕️

Have a nice day 🌞 and keep coding 😉

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence