Make Request Conditional

Updated:

 Note: This is a preview release and subject to change. Feedback welcome! Contact Information and Background (PDF)

also known as: Introduce Request Conditions

Context and Motivation

An API endpoint lets clients access data that rarely changes. Some clients do so rather frequently. Preparing and retransmitting data editions already available to such clients is unnecessary and wasteful.

As an API provider, I want to be able to tell clients that they already have a recent enough version of some data so that I do not have to send this data again.

Stakeholder Concerns (including Quality Attributes and Design Forces)

#performance
Response time, throughput, and processing time are qualities that concern both API clients and providers. Unused data that is prepared, transported, and processed wastes resources and should be avoided.
#data-access-characteristics
As an API client, I do not want to retrieve data repeatedly that I have obtained already.
#developer-experience and #simplicity
Knowing when and how long to cache which data might be challenging for some API clients. Permanent or temporary storage is required. These valid concerns have to be balanced with the provider’s desire for performance.

Initial Position Sketch

In the first message exchange (1–2), the endpoint returns one or more Data Elements (represented by icons). Later on (3), the client requests the data from the endpoint again (for example, because the data should be displayed again). Because nothing has changed, the provider returns the same data (4) as in the previous response.

A single API operation and its request and response messages are targeted by this refactoring.

Smells / Drivers

High latency/poor response time
Load on the API provider is unnecessarily high because the same data is processed and transferred many times over.
Polling proliferation
Clients that participate in long-running conversations and API call orchestrations ping the server for the current status of processing (“are you done?”). They do so more often than the provider-side state advances.

Instructions (Steps)

Instead of transmitting the same data over and over, the request can be made conditional. To allow the communication participants to determine whether the client already has the latest version of the data, they exchange condition information as metadata.

  1. Decide for one of the variants of the Conditional Request pattern to design the condition that is evaluated. For example, data can be timestamped or fingerprinted by calculating a hash code of the response body.
  2. Adjust the API description and implementation to include the condition metadata in both request and response messages. To preserve backwards compatibility and for initial requests of the data, the request metadata should be optional. For the response message, check if the transport protocol provides a special status for this case (such as HTTP status code 304 Not Modified) and use it if present.
  3. In the API implementation, evaluate the condition – for example, by comparing the previously mentioned timestamps or hashes – and respond with an appropriate message.
  4. Create additional unit or integration tests for the API implementation that validate all combinations of metadata presence or absence (with changed and unchanged data elements).
  5. If several operations in the API use Conditional Requests, investigate whether your Web framework offers a way to implement this functionality in a generic way. For instance, the Conditional Request pattern has an example that uses the Spring framework.
  6. Adjust the API client implementations and their tests to utilize the new feature.
  7. Document the changes and release a new API version.

Target Solution Sketch (Evolution Outline)

Comparing our solution to the initial position sketch, we can see that the first exchange (1–2) is unchanged. In the second request, the clients includes the condition metadata (3) in its request, which in turn allows the provider to respond with a special “not modified” message (4):

The “not modified” information is shown as icon in the Figure.

Example(s)

The Customer Core microservice of the Lakeside Mutual sample application implements conditional requests in its WebConfiguration. The fingerprint-based variant is applied in its request and response messages:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    ...

    /**
     * This is a filter that generates an ETag value based on the content of the response. This ETag is compared to the If-None-Match header
     * of the request. If these headers are equal, the response content is not sent, but rather a 304 "Not Modified" status instead.
     * */
    @Bean
    public Filter shallowETagHeaderFilter() {
        return new ShallowEtagHeaderFilter();
    }
}

The ShallowEtagHeaderFilter class is already included in the Spring Framework. Because it is implemented as a filter applied to all requests and responses, the implementation of the individual operations does not have to be adjusted. A consequence of this implementation – and the reason why it is called “shallow” ETag – is that responses are still assembled, hashed and replaced with a 304 Not Modified response if the hash matches the ETag header. Alternatively, a version identifier could be introduced in the (meta-)data to avoid having to retrieve and hash the entire data. This is also supported by Spring Data REST for classes that have an @Version property:

@Entity
public class CustomerAggregateRoot implements RootEntity {

    @Version
    Long version; 

    @EmbeddedId
    private CustomerId id;

    ...
}

Hints and Pitfalls to Avoid

Before and when making requests conditional, ask yourself:

  • How does the additional overhead (calculation of hashes, additional storage used by timestamps) compare to the expected savings?
  • Does the condition cover all the data returned in the response? For example, when the data contains nested structures, a change in a contained element must be detected. Otherwise, clients might see stale data.
  • How does a Conditional Request count towards a Rate Limit?

Do not blindly start caching all API responses on the client side. Cache design is hard to get right, for instance knowing when to invalidate cache entries is not trivial.

Consider applying the Extract Information Holder refactoring if the operation returns nested data holders that change more or less often than the containing data.

Our catalog includes the Introduce Version Identifier refactoring that focuses on versioning endpoints, not data elements.

RFC 7332 is on Conditional Requests in Hypertext Transfer Protocol (HTTP/1.1).