Bartłomiej Słota

Concurrency control in REST API with Spring Framework

Share on facebook
Share on google
Share on twitter
Share on linkedin

In 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 of 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.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class BookAPITest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private BookRepository bookRepository;

  @Test
  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:

@RestController
@RequestMapping("/books")
class BookController {

  private final PlacingOnHold placingOnHold;

  BookController(PlacingOnHold placingOnHold) {
    this.placingOnHold = placingOnHold;
  }

  @PatchMapping("/{bookId}")
  ResponseEntity updateBookStatus(@PathVariable("bookId") UUID bookId,
                                  @RequestBody 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:

@Test
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 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 locking mechanism. From 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 if particular record has not been modified by someone else in the meantime. This is usually done by adding 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.

@SpringBootTest(webEnvironment = NONE)
@RunWith(SpringRunner.class)
public class OptimisticLockingTest {

  @Autowired
  private BookRepositoryFixture bookRepositoryFixture;

  @Autowired
  private BookRepository bookRepository;

  private PatronId somePatronId = somePatronId();

  @Test(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
@Entity @Table(name = "book")
class BookEntity {
  //... 
  @Version
  private long version;
  //...
} 


  • Pass this version further into domain model. Due to the fact that 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 the Book aggregate.
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;
  }
} 
public interface Book { 
  //...
  Version version()
}


  • Introduce domain-specific or general purpose exception called StaleStateIdentified for concurrent access conflicts. According to Dependency 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 like OptimisticLockingFailureException.
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.
@Component
class JpaBasedBookRepository implements BookRepository {

    private final JpaBookRepository jpaBookRepository;

    //constructor + other methods

    @Override
    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 rejection, as he has tried it one second after?”

Business: “Tell him he had bad luck”

Dev: “What if it is our premium patron”

Business: “Oh, well, we should make a call to him. Yes. In such 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.

@Test
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 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 if response body contains 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.

@RestController
@RequestMapping("/books")
class BookHoldingController {

  private final PlacingOnHold placingOnHold;

  BookHoldingController(PlacingOnHold placingOnHold) {
    this.placingOnHold = placingOnHold;
  }

  @PatchMapping("/{bookId}")
  ResponseEntity updateBookStatus(@PathVariable("bookId") UUID bookId,
                                  @RequestBody 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 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 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 use Last-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. The 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 types
application/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 that ETag 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 in opaque-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 their opaque-tags are equal.


Please find examples in the following table : 

ETag #1ETag #2Strong comparisonWeak comparison
“123”“123”matchmatch
“123”W/”123″no matchmatch
W/”123″W/”123″no matchmatch
W/”123″W/”456″no matchno 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.

@Test
    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.

@RestController
@RequestMapping("/books")
class BookFindingController {

    private final FindingBook findingBook;

    public BookFindingController(FindingBook findingBook) {
        this.findingBook = findingBook;
    }

    @GetMapping("/{bookId}")
    ResponseEntity<?> findBookWith(@PathVariable("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:

.andExpect(jsonPath("$.version").doesNotExist())

and exclude the attribute from serialization with @JsonIgnore annotation:

public class BookView {
    //...
    @JsonIgnore
    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 the header we are going to use, we need to know when should we evaluate 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 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 to GET 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.

@Test
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:

@RestController
@RequestMapping("/books")
class BookHoldingController {

    private final PlacingOnHold placingOnHold;

    BookHoldingController(PlacingOnHold placingOnHold) {
        this.placingOnHold = placingOnHold;
    }

    @PatchMapping(path = "/{bookId}", headers = "If-Match")
    ResponseEntity<?> updateBookStatus(@PathVariable("bookId") UUID bookId,
                                       @RequestBody UpdateBookStatus command,
                                       @RequestHeader(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
        }
    }

    @PatchMapping(path = "/{bookId}", headers = "!If-Match")
    ResponseEntity<?> updateBookStatus(@PathVariable("bookId") UUID bookId,
                                       @RequestBody 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:

@Test
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:

@RestController
@RequestMapping("/books")
class BookHoldingController {

    private final PlacingOnHold placingOnHold;

    BookHoldingController(PlacingOnHold placingOnHold) {
        this.placingOnHold = placingOnHold;
    }

    @PatchMapping(path = "/{bookId}")
    ResponseEntity<?> updateBookStatus(@PathVariable("bookId") UUID bookId,
                                       @RequestBody UpdateBookStatus command,
                                       @RequestHeader(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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *