Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
How do you implement optimistic concurrency in an HTTP API? There are a couple of different ways, regardless of what datastore you’re using in the backend. You can leverage the ETag header in the HTTP Response to return a “version” of the resource that was accessed. When a client then needs to perform some operation on the resource, they send an If-Match header apart of the request with the value being the result of ETag from the initial GET request. Another option is to leverage hypermedia by returning URIs for actions relevant to a resource that include the version apart of the URI. This enables concurrency to be completely transparent and does not require any knowledge from the client.
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.
In a concurrent environment like a web application or HTTP API, you have multiple concurrent requests that could be trying to make state changes to the same resource. The normal flow for optimistic concurrency is that clients will specify the latest version they are aware of when attempting to make a state change. As an example, two clients make a request for an HTTP request of GET /products/abc123
The HTTP API returns the data along with a “version” property. The version indicates the current version of that resource. Now when a client makes a subsequent call to perform any type of action that’s going to result in a state change, it also includes the version. As an example, the client performs an inventory adjustment by making an HTTP call to POST /products/abc123/quantity but it also includes the version it received from the prior GET request.
Now if the second client, which also did a GET request and also had version 15 of the resource, makes a similar HTTP call to do an inventory adjustment. It also includes the version it has, which is 15. However since the first client has already made a successful state change, the version is now 16.
This request by the second client will fail because we’ve implemented optimistic concurrency. There are various ways you can implement ways beyond just using a version number, such as a DateTime or Timestamp that represents the last change or most recent version. Using a relational database, this will be implemented by including the version in the WHERE clause and then getting back the value of affected rows. If no rows were affected then the version isn’t what is the current value.
ETags & If-Match
Another way of passing the version around from server to client is by leveraging the ETag and If-Match headers in the HTTP Response and Request.
A good example of this implementation is with Azure CosmosDB. When you request a document, it will return an _etag property but also include the ETag header in the response. This represents the version of the resource.
Here is what the response body looks like:
Here are the headers from the response.
Since we now have the ETag value, we can now use it when making a subsequent request to perform a state change. To do so, the request must pass the If-Match header with the ETag value.
The Cosmos SDK uses this exactly as illustrated within its API.
Another way to pass around the version is simply by using Hypermedia. Hypermedia is about providing the client with information about what other actions or resources are available based on the resources it’s accessing. This means when a client requests the product resource GET /products/abc123, the server will provide it the URI to where it can do an Inventory Adjustment. Since we’re providing the URI, we can include the current version in the URI.
In the example above, I’m using EventStoreDB, which allows optimistic concurrency by passing the current version when appending an event to the event stream. If the version passed is not the current version of the stream, a WrongExpectedVersionException is thrown.
Here’s an example of what the response body looks like when calling GET /products/abc123
When we want to do an Inventory Adjustment, we aren’t constructing a URI, we simply use the response from the GET and find the URI in the commands array.
There are many different ways to handle optimistic concurrency, and hopefully, this illustrated a couple of different options by using the ETags/If-Match headers as well as leveraging hypermedia.
Developer-level members of my YouTube channel or Patreon get access to the full source for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.