Resource version control using ETag
Controlling versions using HTTP headers
Overview
You’ll find that almost every company has a system that is referred to as “The Source of Truth”. In most cases, it does not mean that this service owns and manages the data by itself, but instead it is a place where other systems reflect their changes.
Within the context of distributed systems, this scenario will imply that systems that depend on and interact with “The Source of Truth” will have to synchronize and make sure they are all on the same page.
Writing a Wiki Document
Let’s consider a scenario (Fig.1) where a user is writing a Wiki document. When the user saves the document it is saved in Wiki’s service (“The Source of Truth”).
Now let’s consider another scenario (Fig.2) where two users are writing and saving the same Wiki file. Which user is going to have his document changes saved?
In this scenario, we can identify that both users are doing the same operation on the same resource resulting in a mid-air collision if they submit the changes at the same time.
Versioning
To make sure a user does not overwrite another user’s changes accidentally, an option is to save versions of the Wiki document.
Let’s say that the Wiki service now saves the version of a document, and to simplify let’s consider that this version is incremental. Then, during the communication between the client and the server, we need to identify the resource version we are working on. We can do so by taking advantage of some HTTP headers.
HTTP headers allow the client and the server to pass supplementary information within an HTTP request or response. A very common header is the ContentType
which is used to indicate the original media type of the resource, for example: ContentType: application/json
.
When dealing with resource versioning, some useful HTTP headers are: Etag
, If-Match
, and If-None-Match.
- The
ETag
(or entity tag) response header is an identifier for a specific version of a resource. It helps to prevent simultaneous updates of a resource from overwriting each other. - The
If-Match
request header makes a request conditional. If the sent version in theIf-Match
header does not match the resource’s version on the server (Etag
), a412 Precondition Failed
response is returned. - The
If-None-Match
request header makes the request conditional and can be used to check if the cached versions are still up-to-date.
In Fig.3, when editing the wiki document, the current version is set into an If-Match
header in the request:
If-Match: "1"
And if the request is successful, the response returns the ETag
header with the new document’s generated version.
ETag: "2"
Then, if another user attempts to save its changes in the same document, Fig.4, it will set the If-Match
header in the request to:
If-Match: "1"
And versions will not match because the latest version of this document is now 2, resulting in a 412 Precondition Failed
response.
Caching
To avoid this mismatch and take advantage of the cached version, the current version that this user thinks is the latest can be set into an If-None-Match
header in the request, Fig.5.
If-None-Match: "2"
In this scenario, the Wiki service will compare the latest version with the one given by the user (in If-None-Match
header), and if it matches it will result in a 304 Not Modified
, which tells the user that his version has not been changed by anyone else and is still fresh.
However, if the versions mismatch, the response is 412 Precondition Failed
and the user has to GET
the latest document version and update its document before submitting a change.
Microservices Ecosystem
In the previous examples, two users were writing in the same wiki document, and, that’s not much different than a microservice distributed environment where services have to interact with each other and also contain a “The Source of Truth” service.
Let’s consider a company that has a service responsible for managing client contracts, we’re calling it Clients API.
Commonly, there are some flows to create a new client contract, and depending on the client it can be created via:
Back office
: details are filled manually, validated, and submitted to the Clients API.Self-Checkout
: automatic flow to purchase and operate the contract. In its last step, the data is submitted to the Clients API. (e.g.: flow developed by the company).
Thus, regardless of the client contract flow, there must be synchronization with the Clients API which is “The Source of Truth” service, Fig.6.
To validate any operation on a Client contract, its version is sent using the If-Match
and ETag
headers.
We may also use If-None-Match
to validate if the cached version is still up-to-date and update the respective User Interface
accordingly using a polling strategy for example.
The versioning control behavior is similar to the Wiki document example.
Finally, we can guarantee that there are no collisions or unexpected overwrites on a Client contract.