Concurrency Control In REST API With Spring Framework
Take a look at this lengthy tutorial on concurrency control in the REST API with Spring in the context of a book borrowing service.
Join the DZone community and get the full member experience.
Join For FreeIn modern software systems, it is not uncommon to have hundreds or thousands of users independently and concurrently interacting with our resources. We generally want to avoid situations when changes made by one client are overridden by another one without even knowing. In order to prevent our data integrity from being violated we often use locking mechanisms provided by our database engine, or even use abstractions provided by tools like JPA.
Have you ever wondered how concurrency control should be reflected in our API? What happens when two users update the same record at the same time? Will we send any error message? What HTTP response code will we use? What HTTP headers will we attach?
The aim of this article is to give comprehensive instructions on how to model our REST API so that it supports concurrency control of our resources and utilizes features of HTTP protocol. We will also implement this solution with the help of Spring Framework.
Please note that although we make a short introduction into concurrent data access, this article does not cover any internals of how locks, isolation levels, or transactions work. We will strictly focus on API.
Code examples used in this article you will find here.
Use Case
Use case that we will be working on is based on DDD reference project – library. Imagine we have a system automating the process of placing books on hold by patrons. For the sake of simplicity, let’s assume that each book can be in one of two possible states: available and placed on hold. A book can be placed on hold only if it exists in the library and is currently available. This is how it could be modeled during EventStorming session:
Each patron can place the book on hold (send a command). In order to make such decision, he/she needs to see the list of available books first (view a read model). Depending on the invariant, we will either allow or disallow the process to succeed.
Let’s also assume, that we made a decision to make Book
our main aggregate. The above process visualized with Web Sequence Diagrams
could look like this:
Like we can see in this diagram, Bruce successfully places the book 123
on hold, while Steve needs to handle 4xx
exception. What xx
should we put here? We will get back to it in a second.
Let’s start with providing the minimum viable product not paying attention to concurrent access for a moment. Here is how our simple test could look like.
xxxxxxxxxx
webEnvironment = RANDOM_PORT) (
SpringRunner.class) (
public class BookAPITest {
private MockMvc mockMvc;
private BookRepository bookRepository;
public void shouldReturnNoContentWhenPlacingAvailableBookOnHold() throws Exception {
//given
AvailableBook availableBook = availableBookInTheSystem();
//when
ResultActions resultActions = sendPlaceOnHoldCommandFor(availableBook.id());
//then
resultActions.andExpect(status().isNoContent());
}
private ResultActions sendPlaceOnHoldCommandFor(BookId id) throws Exception {
return mockMvc
.perform(patch("/books/{id}", id.asString())
.content("{"status" : "PLACED_ON_HOLD"}")
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE));
}
private AvailableBook availableBookInTheSystem() {
AvailableBook availableBook = BookFixture.someAvailableBook();
bookRepository.save(availableBook);
return availableBook;
}
}
And here is how its implementation could look like:
xxxxxxxxxx
"/books") (
class BookController {
private final PlacingOnHold placingOnHold;
BookController(PlacingOnHold placingOnHold) {
this.placingOnHold = placingOnHold;
}
"/{bookId}") (
ResponseEntity updateBookStatus( ("bookId") UUID bookId,
UpdateBookStatus command) {
if (PLACED_ON_HOLD.equals(command.getStatus())) {
placingOnHold.placeOnHold(BookId.of(bookId));
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.ok().build(); //we do not care about it now
}
}
}
We could also complement our test class with one more check:
xxxxxxxxxx
public void shouldReturnBookOnHoldAfterItIsPlacedOnHold() throws Exception {
//given
AvailableBook availableBook = availableBookInTheSystem();
//and
sendPlaceOnHoldCommandFor(availableBook.id());
//when
ResultActions resultActions = getBookWith(availableBook.id());
//then
resultActions.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(availableBook.id().asString()))
.andExpect(jsonPath("$.status").value("PLACED_ON_HOLD"));
}
State Comparison vs Locking
All right. We have just provided the functionality of placing a book on hold. Aggregates in Domain Driven Design, however, are supposed to be the fortress of invariants – their main roles are to keep all business rules always fulfilled and provide atomicity of operations. One of the business rules we discovered and described in previous section is that a book can be placed on hold if and only if it is available. Is this rule always fulfilled?
Well, let’s try to analyze it. First thing we provided in our code is a type system – a concept borrowed from functional programming. Instead of having one multipurpose Book
class with a status field, and tons of if statements, we delivered AvailableBook
and PlacedOnHoldBook
classes instead. In this setup, it is only AvailableBook
that has the placeOnHold
method. Is it enough for our application to protect the invariant?
If two different patrons try to place the same book on hold sequentially – the answer is yes, as it will be the compiler that will support us here. Otherwise, we need to handle concurrent access anyway – and this is what we are going to do now. We have two possible options here: full state comparison and locking. In this article we will briefly walk through the former option, focusing much more on the latter.
Full State Comparison
What is hidden behind this term? Well, if we want to protect ourselves from so-called lost updates what we need to do while persisting the state of our aggregate is to check if the aggregate we want to update hasn’t been changed by someone else in the meantime. Such check could be done by comparing aggregate’s attributes from before the update with what is currently in the database. If the result of the comparison is positive, we can persist the new version of our aggregate. These operations (comparing and updating) need to be atomic.
The advantage of this solution is that it does not impact the structure of an aggregate – technical persistence details do not leak into domain layer or any other layer above. However, as we need to have the previous state of an aggregate to make full comparison, we need to pass this state to our persistence layer through the repository port. This, in turn, impacts the signature of repository save
method, and requires adjustments in the application layer as well. Nevertheless, it is way cleaner than the second solution, which you will see in the following paragraph. Before we move on, it is also worth noting, that this solution bears the burden of potentially computationally heavy searches on the database. If our aggregate is big, maintaining a full index on our database might be painful. Functional indexes might come to a rescue.
Locking
The second option is to use a locking mechanism. From a high-level perspective, we can distinguish two types of locking: pessimistic and optimistic.
The former type is that our application acquires either exclusive or shared lock on particular resources. If we want to modify some data, having an exclusive lock is the only option. Our client can then manipulate resources, not letting any other one to even read the data. Shared lock, however, does not let us manipulate resources, and is a bit less restrictive for other clients, which can still read the data.
On the contrary, optimistic locking lets every client read and write data at will with the restriction that just before committing the transaction we need to check whether a particular record has not been modified by someone else in the meantime. This is usually done by adding the current version or last modification timestamp attribute.
When the number of write operations is not that big comparing to read operations, optimistic locking is often a default choice.
Optimistic Locking in Data Access Layer
In the Java world it is usually JPA that we utilize in order to handle data access including locking capabilities. Optimistic locking in JPA can be enabled by declaring a version attribute in an entity and marking it with a @Version
annotation. Let’s have a look at how it could look like, starting with a test.
xxxxxxxxxx
webEnvironment = NONE) (
SpringRunner.class) (
public class OptimisticLockingTest {
private BookRepositoryFixture bookRepositoryFixture;
private BookRepository bookRepository;
private PatronId somePatronId = somePatronId();
expected = StaleStateIdentified.class) (
public void savingEntityInCaseOfConflictShouldResultInError() {
//given
AvailableBook availableBook = bookRepositoryFixture.availableBookInTheSystem();
//and
AvailableBook loadedBook = (AvailableBook) bookRepository.findBy(availableBook.id()).get();
PlacedOnHoldBook loadedBookPlacedOnHold = loadedBook.placeOnHoldBy(somePatronId);
//and
bookWasModifiedInTheMeantime(availableBook);
//when
bookRepository.save(loadedBookPlacedOnHold);
}
private void bookWasModifiedInTheMeantime(AvailableBook availableBook) {
PatronId patronId = somePatronId();
PlacedOnHoldBook placedOnHoldBook = availableBook.placeOnHoldBy(patronId);
bookRepository.save(placedOnHoldBook);
}
}
In order to make this test pass we needed to provide a few things:
- Introduce aforementioned version attribute in JPA
BookEntity
in infrastructure layer
xxxxxxxxxx
name = "book") (
class BookEntity {
//...
private long version;
//...
}
- Pass this version further into the domain model. Due to the fact that the domain model defines repository (interface) based on domain-specific abstractions, in order to make it possible for infrastructure (JPA) to check the entity version is to have this version in the domain as well. For this purpose we introduced
Version
value object, and added it into theBook
aggregate.
xxxxxxxxxx
public class Version {
private final long value;
private Version(long value) {
this.value = value;
}
public static Version from(long value) {
return new Version(value);
}
public long asLong() {
return value;
}
}
xxxxxxxxxx
public interface Book {
//...
Version version()
}
- Introduce domain-specific or general-purpose exception called
StaleStateIdentified
for concurrent access conflicts. According toDependency Inversion Principle
, modules with higher level of abstraction should not depend upon modules with lower level of abstractions. That’s why we should place it either in domain module or in a supporting one, but not in the infrastructure. This exception should be instantiated and raised by infrastructure adapters, as a result of translation of low-level exception likeOptimisticLockingFailureException
.
xxxxxxxxxx
public class StaleStateIdentified extends RuntimeException {
private StaleStateIdentified(UUID id) {
super(String.format("Aggregate of id %s is stale", id));
}
public static StaleStateIdentified forAggregateWith(UUID id) {
return new StaleStateIdentified(id);
}
}
- Instantiate and raise the exception in infrastructure adapters, as a result of translation of low-level exception like
OptimisticLockingFailureException
.
xxxxxxxxxx
class JpaBasedBookRepository implements BookRepository {
private final JpaBookRepository jpaBookRepository;
//constructor + other methods
public void save(Book book) {
try {
BookEntity entity = BookEntity.from(book);
jpaBookRepository.save(entity);
} catch (OptimisticLockingFailureException ex) {
throw StaleStateIdentified.forAggregateWith(book.id().getValue());
}
}
}
interface JpaBookRepository extends Repository<BookEntity, UUID> {
void save(BookEntity bookEntity);
}
All right. Our test passes now. The question now is what happens in our API if StaleStateIdentified
is raised? By default, 500 INTERNAL SERVER ERROR
status will be returned, which is definitely not something we would like to see. It is high time we moved to handling the StaleStateIdentified
exception, then.
Handling Optimistic Locking in REST API
What should happen in case of concurrent access conflict? What our API should return? What our end user should see?
Before we propose a solution, let’s emphasize, that in most cases answers to these questions shouldn’t be given by developers, because such conflict is usually a business problem, not a technical one (even if we strongly believe it is). Let’s have a look at following example:
Dev: “What should we do if two patrons try to place the same book on hold, and one of them gets rejected, as he has tried it one second after?”
Business: “Tell him too bad.”
Dev: “What if it is our premium patron?”
Business: “Oh, well, we should make a call to him. Yes. In such a situation send me an email, I will contact him, and apologize for it, trying to find some other copy for him.”
We could find countless examples proving that the technical solution should always be driven by the real business rules.
To keep things simple, let’s assume, that we just want to tell our customer that we’re sorry. The very basic mechanism provided by HTTP protocol we can find in RFC 7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content and it is about returning 409 CONFLICT
response. Here is what is stated in the document:
The
409 (Conflict)
status code indicates that the request could not
be completed due to a conflict with the current state of the target
resource. This code is used in situations where the user might be
able to resolve the conflict and resubmit the request. The server
SHOULD generate a payload that includes enough information for
a user to recognize the source of the conflict.
Conflicts are most likely to occur in response to a PUT request. For
example, if versioning were being used and the representation being
PUT included changes to a resource that conflict with those made by
an earlier (third-party) request, the origin server might use a 409
response to indicate that it can’t complete the request. In this
case, the response representation would likely contain information
useful for merging the differences based on the revision history.
Isn’t it something we are looking for? All right, then. Let’s try to write a test that reflects what’s written above.
xxxxxxxxxx
public void shouldSignalConflict() throws Exception {
//given
AvailableBook availableBook = availableBookInTheSystem();
//and
BookView book = api.viewBookWith(availableBook.id());
//and
AvailableBook updatedBook = bookWasModifiedInTheMeantime(bookIdFrom(book.getId()));
//when Bruce places book on hold
PatronId bruce = somePatronId();
ResultActions bruceResult = api.sendPlaceOnHoldCommandFor(book.getId(), bruce,
book.getVersion());
//then
bruceResult
.andExpect(status().isConflict())
.andExpect(jsonPath("$.id").value(updatedBook.id().asString()))
.andExpect(jsonPath("$.title").value(updatedBook.title().asString()))
.andExpect(jsonPath("$.isbn").value(updatedBook.isbn().asString()))
.andExpect(jsonPath("$.author").value(updatedBook.author().asString()))
.andExpect(jsonPath("$.status").value("AVAILABLE"))
.andExpect(jsonPath("$.version").value(not(updatedBook.version().asLong())));
}
What happens here is that the first thing we do with a book that is available in the system is getting its view. In order to enable concurrent access control, the view response needs to contain version attribute corresponding to one we already have in our domain model. Amongst others, it is included in a command that we send to place the book on hold. In the meantime, though, we modify the book (forcing version attribute to be updated). As a result, we expect to get a 409 CONFLICT
response indicating that the request could not be completed due to a conflict with the current state of the target resource. Moreover, we expect that the response representation would likely contain information useful for merging the differences based on the revision history, and that’s why we check whether the response body contains the current state of the book.
Please note that in the last line of the test method we do not check the exact value of version
. The reason behind it is that in the context of REST controller we do not (and should not) care about how this attribute is calculated and updated – the fact that it changes is enough information. Thus, we address separation of concerns in tests.
After we have a test ready, we can update our REST controller now.
xxxxxxxxxx
"/books") (
class BookHoldingController {
private final PlacingOnHold placingOnHold;
BookHoldingController(PlacingOnHold placingOnHold) {
this.placingOnHold = placingOnHold;
}
"/{bookId}") (
ResponseEntity updateBookStatus( ("bookId") UUID bookId,
UpdateBookStatus command) {
if (PLACED_ON_HOLD.equals(command.getStatus())) {
PlaceOnHoldCommand placeOnHoldCommand =
new PlaceOnHoldCommand(BookId.of(bookId), command.patronId(), command.version());
Result result = placingOnHold.handle(placeOnHoldCommand);
return buildResponseFrom(result);
} else {
return ResponseEntity.ok().build(); //we do not care about it now
}
}
private ResponseEntity buildResponseFrom(Result result) {
if (result instanceof BookPlacedOnHold) {
return ResponseEntity.noContent().build();
} else if (result instanceof BookNotFound) {
return ResponseEntity.notFound().build();
} else if (result instanceof BookConflictIdentified) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(((BookConflictIdentified) result)
.currentState()
.map(BookView::from)
.orElse(null));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
First validation in updateBookStatus
method is to check whether it is a request for placing a book on hold or not. If so, a command object is built and passed further to application layer service – placingOnHold.handle()
. Based on the result of the service invocation we can build proper API response. If processing was successful (BookPlacedOnHold
) we just return 204 NO_CONTENT
. If the request tries to modify not existing resource (BookNotFound
) we return 404 NOT_FOUND
. The third, and most important in our context option is BookConflictIdentified
. If we get such a response, our API returns 409 CONFLICT
message, with body containing the latest book view. Any other result of command handling is at this point not expected and treated as 500 INTERNAL_SERVER_ERROR
.
If a consumer gets 409
, it needs to interpret the status code and analyze the content in order to determine what might have been the source of the conflict. According to RFC 5789, these are the application and the patch
format that determine if a consumer can reissue the request as it is, recalculate a patch, or fail. In our case, we are not able to perform a retry of the message preserving its form. The reason behind this is that the version
attribute has been changed. Even if we apply the new version, before resending our message we need to check the source of the conflict – we are allowed to do it only if the conflict was not caused by changing the status of the book to PLACED_ON_HOLD
(we can only place available books on hold). Any other change (title, author, etc.) not affecting the status would not impact the business invariant, allowing the consumer to reissue the request.
It is worth pointing out differences between using optimistic locking with version
attribute passed to API and state comparison. The bad thing is that the version attribute needs to be added to our domain, application, and API levels, causing the leak of technical details from persistence layer. The good thing, though, is that now in order to perform the update, the WHERE
clause can be limited to aggregate ID
and version
fields. The simplification is based on the fact that the state is now represented by one parameter instead of a whole set. Regarding the API response in case of a conflict, the situation is pretty much the same. Both methods force our client to analyze the response and make the decision whether retransmission is possible or not.
Looking at this problem pragmatically, we could give a couple of arguments in favor of using optimistic locking.
- Domain is dirty, but the API is clear, concise, and it is way easier to use preconditions (more on this topic in further chapters)
Version
can sometimes be desired by the business for purposes like auditing for example, so we could potentially gain even more- If
version
is still hard to be accepted, we could useLast-Modified
attribute and send it in a header. In many businesses the time of the last modification of a resource might have more meaning.
ETag Header
Have you spotted that in both of previously mentioned methods we actually execute conditional update on database? Doesn’t it mean that our request is conditional? Yes, it does, because we allow our clients to update the book only if it has not been modified in the meantime. In the first case, we need to compare all attributes of an aggregate, while in the second one, we only check if version
and aggregate ID
are the same. All attributes consistency and version-based consistency both define a pre-condition for the request to be fulfilled.
There is an explicit and standard way of dealing with conditional requests in HTTP protocol. RFC 7232 defines this concept including a set of metadata headers indicating the state of the resource and pre-conditions:
Conditional requests are HTTP requests [RFC7231] that include one or more header fields indicating a precondition to be tested before applying the method semantics to the target resource.
RFC 7232 distinguishes conditional read and write requests. The former ones are often used for effective caching mechanisms, which is out of the scope of this article. The latter requests are what we are going to focus on. Let’s continue with some theory.
The very basic component of conditional request handling is the ETag
(Entity Tag
) header that should be returned anytime we read a resource with a GET
request or update it with some unsafe method. ETag
is an opaque textual validator (token) generated by the server owning the resource and associated with its particular representation at a current point in time. It must enable unique identification of the state of the resource. Ideally, every change in both entity state (response body) and its metadata (e.g. content type) is reflected in updated ETag
value.
One could ask: why do we need an ETag
when we have a Last-Modified
header? There are a couple of reasons actually, but from the perspective of unsafe methods execution it is worth noting that according to RFC 7231 Last-Modified
header schema limits the time resolution to seconds only. In situations where it is not sufficient, we simply cannot rely on it.
ETag Validation
We will start describing ETag
from its validation instead of generation not by accident. In a nutshell, the way we create it depends on a chosen way of validating preconditions. There are two types of validations – strong (default) and weak.
An ETag
is considered strong when its value is updated whenever the content of particular resource representation changes and is observable in 200 OK
response to GET
request. It is crucial that the value is unique between different representations of the same resource unless these representations have identical form of serialized content. To be more specific: if a particular resource’s bodies represented in typesapplication/vnd+company.category+json
and application/json
are both identical, they can share the same ETag
value, forcing different values to be used otherwise.
An ETag
is considered weak when its value might not be updated on every change of the resource representation. The reasons for using weak tags might be dictated by the limitations of the algorithm calculating them. As an example, we could take timestamp resolution or inability to ensure uniqueness across different representations of the same resource.
Which ETag
should we use? It depends. Strong ETags
can be difficult, or even impossible to generate efficiently. Weak ETags
, however, are considered easier to generate but less reliable in terms of resource state comparison. The choice should be dictated by the specifics of our data, types of supported representation media types, and what is most important – our ability to ensure uniqueness across different representations of a single resource.
ETag Generation
ETag is supposed to be built according to following pattern:
ETag = [W/]"{opaque-tag}"
The pattern looks simple, but it requires some clarification:
W/
is case sensitive weak validation optional indicator. If present – it informs thatETag
will be validated as weak one. The more on this we will find in following section of the article.opaque-tag
is mandatory string value, surrounded by double quotes. Due to escaping/unescaping problems between servers and clients it is advised to avoid double quotes inopaque-tags
.
Below we will find a couple examples of valid ETags:
""
"123"
W/"my-weak-tag"
As we can see, ETag
might contain lots of types of things, but the question now is: how should we generate it? What should we put in place of the opaque-tag? It might be an implementation-specific version number combined with content type classifier, a hash value calculated from the content’s representation. It can be even a timestamp with sub-second resolution.
Comparison
As we already know how to generate weak and strong ETags
, the only thing we miss now is how to actually check if a given value passes respective validations. There is a rule:
- Two
ETags
are equal in strong comparison if and only if neither of them is weak and their values are identical. - Two
ETags
are equal in weak comparison if theiropaque-tags
are equal.
Please find examples in the following table :
ETag #1 | ETag #2 | Strong comparison |
Weak comparison
|
“123” | “123” | match | match |
“123” | W/”123″ | no match | match |
W/”123″ | W/”123″ | no match | match |
W/”123″ | W/”456″ | no match | no match |
Getting into implementation, let’s start with a test checking if the representation of a book contains the ETag
header. In our example, we will generate it straight from book’s version attribute. To keep things simple, let’s also assume that there is only one representation supported and we omit it in this process.
xxxxxxxxxx
public void shouldIncludeETagBasedOnVersionInBookViewResponse() throws Exception {
//given
Version version = someVersion();
AvailableBook availableBook = bookRepositoryFixture.availableBookInTheSystemWith(version);
//when
ResultActions resultActions = api.getBookWith(availableBook.id());
//then
resultActions
.andExpect(status().isOk())
.andExpect(header().string(ETAG, String.format("\"%d\"", version.asLong())));
}
In order to make this test pass, we need to include the header while building the response.
xxxxxxxxxx
"/books") (
class BookFindingController {
private final FindingBook findingBook;
public BookFindingController(FindingBook findingBook) {
this.findingBook = findingBook;
}
"/{bookId}") (
ResponseEntity<?> findBookWith( ("bookId") UUID bookIdValue) {
Optional<BookView> book = findingBook.findBy(BookId.of(bookIdValue));
return book
.map(it -> ResponseEntity.ok().eTag(ETag.of(Version.from(it.getVersion())).getValue()).body(it))
.orElse(ResponseEntity.notFound().build());
}
}
As we can see it, there is a eTag()
method in the response builder which we can utilize to set the header of our choice. Spring framework provides automatic support for managing ETag headers but it is limited to cache control mechanism. The unsafe method processing is up to us to deliver.
If we build ETag
based on the version
attribute, we might no longer need it in the response body (assuming it has no business value). Thus, we could enhance our test with following assertion:
xxxxxxxxxx
.andExpect(jsonPath("$.version").doesNotExist())
and exclude the attribute from serialization with @JsonIgnore
annotation:
xxxxxxxxxx
public class BookView {
//...
private final long version;
//...
}
Finally, we could get rid of this field from the command, but let’s just leave it for now, as this has further consequences.
Preconditions
We know what ETags
are, how to calculate and compare them. Now it is time for conditional requests. In order to create a conditional request we need to utilize ETag
returned by the server, and put its value into one of conditional headers: If-Match
, If-Not-Matched
, If-Modified-Since
, If-Unmodified-Since
, or If-Range
. In this article we will focus only on If-Match
, and If-Unmodified-Since
headers, as these are the only ones applicable for unsafe methods.
Evaluation
Regardless of the header that we use, we need to know when should we evaluate the conditions embedded in these headers. Here is what we can find in RFC 7232:
A server MUST ignore all received preconditions if its response to the same request without those conditions would have been a status code other than a 2xx (Successful) or 412 (Precondition Failed). In other words, redirects and failures take precedence over the evaluation of preconditions in conditional requests.
It means that if we have any validations on the server-side that end up in 404
, 422
, or 4xx
messages in general being returned, we should perform them first. We need to remember, though, that precondition checks must also take place before applying the actual method semantics on the target resource.
If-Match
The idea of the If-Match
header is to give the server information about what representation of particular resource the client expects it to have. If-Match
header can be equal to:
*
– any representation of the response is fine, which has the lowest (if any) degree of usefulness in our case- one particular
ETag
value retrieved previously from the response toGET
request - comma-separated list of
ETag
values
In our case the most appropriate choice is to use the If-Match
header with the single ETag
value. Let’s write a test.
xxxxxxxxxx
public void shouldSignalPreconditionFailed() throws Exception {
//given
AvailableBook availableBook = availableBookInTheSystem();
//and
ResultActions bookViewResponse = api.getBookWith(availableBook.id());
BookView book = api.parseBookViewFrom(bookViewResponse);
String eTag = bookViewResponse.andReturn().getResponse().getHeader(ETAG);
//and
bookWasModifiedInTheMeantime(bookIdFrom(book.getId()));
//when Bruce places book on hold
PatronId bruce = somePatronId();
TestPlaceOnHoldCommand command = placeOnHoldCommandFor(book.getId(), bruce, book.getVersion())
.withIfMatchHeader(eTag);
ResultActions bruceResult = api.send(command);
//then
bruceResult.andExpect(status().isPreconditionFailed());
}
In order to make this test pass we need to apply a few changes in BookHoldingController
:
xxxxxxxxxx
"/books") (
class BookHoldingController {
private final PlacingOnHold placingOnHold;
BookHoldingController(PlacingOnHold placingOnHold) {
this.placingOnHold = placingOnHold;
}
path = "/{bookId}", headers = "If-Match") (
ResponseEntity<?> updateBookStatus( ("bookId") UUID bookId,
UpdateBookStatus command,
name = HttpHeaders.IF_MATCH) ETag ifMatch) { (
if (PLACED_ON_HOLD.equals(command.getStatus())) {
Version version = Version.from(Long.parseLong(ifMatch.getTrimmedValue()));
PlaceOnHoldCommand placeOnHoldCommand = PlaceOnHoldCommand.commandFor(BookId.of(bookId), command.patronId())
.with(version);
Result result = placingOnHold.handle(placeOnHoldCommand);
return buildConditionalResponseFrom(result);
} else {
return ResponseEntity.ok().build(); //we do not care about it now
}
}
path = "/{bookId}", headers = "!If-Match") (
ResponseEntity<?> updateBookStatus( ("bookId") UUID bookId,
UpdateBookStatus command) {
//...
}
private ResponseEntity<?> buildConditionalResponseFrom(Result result) {
if (result instanceof BookPlacedOnHold) {
return ResponseEntity.noContent().build();
} else if (result instanceof BookNotFound) {
return ResponseEntity.notFound().build();
} else if (result instanceof BookConflictIdentified) {
return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build();
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private ResponseEntity<?> buildResponseFrom(Result result) {
if (result instanceof BookPlacedOnHold) {
return ResponseEntity.noContent().build();
} else if (result instanceof BookNotFound) {
return ResponseEntity.notFound().build();
} else if (result instanceof BookConflictIdentified) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(((BookConflictIdentified) result)
.currentState()
.map(BookView::from)
.orElse(null));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
Instead of modifying existing method (the one that used to return 409
in case of version conflict) we added new one, that requires If-Match
header to be present. There are two reasons for that. The first one is that we can deploy our new approach without breaking clients of our API. Secondly, we can let our clients choose if they want to utilize the goods of conditional requests or stick to the “classic” solution. The second solution bears the burden of keeping the version attribute within the PATCH
request body.
Missing Preconditions
And here we reach the moment when we need to decide whether we want to keep this two solutions running in parallel. Is there a way to force the API clients to use conditional requests?
In RFC 6585 we can read:
The 428 status code indicates that the origin server requires the request to be conditional.
Its typical use is to avoid the “lost update” problem, where a client GETs a resource’s state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict. By requiring requests to be conditional, the server can assure that clients are working with the correct copies.
When we decide to force pre-conditions to be used, we start with the following test:
xxxxxxxxxx
public void shouldSignalPreconditionRequiredWhenIfMatchIsHeaderMissing() throws Exception {
//given
AvailableBook availableBook = bookRepositoryFixture.availableBookInTheSystem();
//when
TestPlaceOnHoldCommand command = placeOnHoldCommandFor(availableBook, patronId).withoutIfMatchHeader();
ResultActions resultActions = api.send(command);
//then
resultActions
.andExpect(status().isPreconditionRequired())
.andExpect(jsonPath("$.message").value(equalTo("If-Match header is required")));
}
In order to make this test pass, we need to get rid of all things related to handling 409 CONFLICT
. After the cleanup, our controller would look as follows:
xxxxxxxxxx
"/books") (
class BookHoldingController {
private final PlacingOnHold placingOnHold;
BookHoldingController(PlacingOnHold placingOnHold) {
this.placingOnHold = placingOnHold;
}
path = "/{bookId}") (
ResponseEntity<?> updateBookStatus( ("bookId") UUID bookId,
UpdateBookStatus command,
name = HttpHeaders.IF_MATCH, required = false) ETag ifMatch) { (
if (PLACED_ON_HOLD.equals(command.getStatus())) {
return Optional.ofNullable(ifMatch)
.map(eTag -> handle(bookId, command, eTag))
.orElse(preconditionFailed());
} else {
return ResponseEntity.ok().build(); //we do not care about it now
}
}
private ResponseEntity<?> handle(UUID bookId, UpdateBookStatus command, ETag ifMatch) {
Version version = Version.from(Long.parseLong(ifMatch.getTrimmedValue()));
PlaceOnHoldCommand placeOnHoldCommand = PlaceOnHoldCommand.commandFor(BookId.of(bookId), command.patronId())
.with(version);
Result result = placingOnHold.handle(placeOnHoldCommand);
return buildResponseFrom(result);
}
private ResponseEntity<?> buildResponseFrom(Result result) {
if (result instanceof BookPlacedOnHold) {
return ResponseEntity.noContent().build();
} else if (result instanceof BookNotFound) {
return ResponseEntity.notFound().build();
} else if (result instanceof BookConflictIdentified) {
return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED).build();
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private ResponseEntity preconditionFailed() {
return ResponseEntity
.status(HttpStatus.PRECONDITION_REQUIRED)
.body(ErrorMessage.from("If-Match header is required"));
}
}
Preconditions Precedence
Like we have already mentioned, If-Match
header is not the only option to be used when it comes to conditional unsafe requests. If we decide to return Last-Modified
header for GET
requests, the corresponding conditional request header is If-Unmodified-Since
. According to RFC 7232, If-Unmodified-Since
can be validated on the server-side only when there is no If-Match header included in the request.
Conclusions
In multi-user environments, dealing with concurrent access is our bread and butter. Concurrency control could and should be reflected in our API, especially because HTTP provides a set of headers and response codes to support it.
The first option to go for is to add version attribute into our read model, and pass it further in our unsafe methods. In case of detecting collision on server side, we could return 409 CONFLICT
status with a message containing all necessary information to let the client know what is the source of the problem.
A bit more advanced solution are conditional requests. GET
methods should return ETag
or Last-Modified
headers, and their values should be put accordingly to If-Match
or If-Unmodified-Since
headers of unsafe methods. In case of conflict, server returns 412 PRECONDITION FAILED
.
If we want to force our clients to use conditional requests, in case of missing preconditions, server returns 428 PRECONDITION REQUIRED
.
Spring Framework does not support us in modeling concurrent access in our API out of the box. Nevertheless, driving our API by tests showed that the very basic mechanisms available in Spring Web make it be at our fingertips.
Published at DZone with permission of Bartłomiej Słota, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments