Make Request Conditional
also known as: Introduce Request Conditions, Cache Responses
Context and Motivation
An API endpoint provides data that changes rarely, and thus, some clients request and receive the same data frequently. Preparing and retransmitting data already available to the clients is unnecessary and wasteful.
As an API provider, I want to be able to tell clients that they already have the most recent version of certain data so that I do not have to send this data again.
Stakeholder Concerns
- #performance, #green-software
- Response, throughput, and processing times concern API clients and providers. Unused data that is prepared, transported, and processed wastes resources, which should be avoided.
- #data-access-characteristics
- API clients might use caching and do not want to retrieve data they already have.
- #developer-experience, #simplicity
- Knowing when and how long to cache which data might be challenging for API clients and providers. Permanent or temporary storage is required. These valid concerns have to be balanced with the desire for performance.
Initial Position Sketch
Figure 1 shows the initial position sketch for this refactoring. The client requests some data from the API. Later, the client wants to ensure that the data is still up to date and sends a second request for the same data.
Figure 1: Make Request Conditional: Initial Solution Sketch: In the first message exchange (1–2), the endpoint returns one or more Data Elements. Later on (3), the client requests the data from the endpoint again. Because nothing has changed, the provider returns the same data (4) as in the previous response.
This refactoring targets a single API operation and its request and response messages.
Design Smells
- High latency/poor response time
- Load on the API provider is unnecessarily high because the same data is processed and transferred many times over.
- Spike load
- Regular requests for large amounts of data can cause Periodic Workload or Unpredictable Workload [Fehling et al. 2014] for CPU and memory, for instance, when a relatively large JSON object representing the requested data has to be constructed (on the provider side) and read (on the client side).
- 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
Instead of transmitting the same data repeatedly, the request can be conditional. Condition information is exchanged as metadata to allow the communication participants to determine whether the client already has the latest data version.
- Decide for one of the two variants of the Conditional Request pattern: data can a) be timestamped or b) responses be fingerprinted (by calculating a hash code of the response body) [Zimmermann et al. 2022].
- Adjust the API specification and implementation to include a conditional Metadata Element in both request and response messages. The request metadata should be optional so that it can be omitted in initial requests; optionality also brings backward compatibility. For the response message, check if the transport protocol provides a special status for this case and consider using it (such as HTTP status code
304 Not Modified
). - In the API implementation, evaluate the condition – for example, by comparing the previously mentioned timestamps or fingerprints/hashes – and respond with an appropriate message.
- Create additional unit or integration tests for the API implementation that validate combinations of metadata presence or absence (with changed and unchanged data).
- If several operations in the API use Conditional Requests, investigate whether your framework offers a way to implement this functionality in a generic way.
- Adjust the API client implementations that you oversee (for instance, API usage examples) to utilize the new feature: send conditions and keep previously received data. Adjust the API tests as well.
- Document the changes, for example, in a changelog data release notes, and release a new API version.
This refactoring can be applied incrementally, for instance, to a single operation or a group of operations. Backwards compatibility is preserved by making the condition metadata optional in the request.
Target Solution Sketch (Evolution Outline)
Comparing the solution in Figure 2 to the initial position sketch, we see that follow-up requests return a special response message indicating that the data has not changed. The client can then continue to use the data it has already received.
Figure 2: Make Request Conditional: Target Solution Sketch: The first exchange (1–2) is the same as in the initial position. In the second request though, the client includes the condition metadata (3) in its request, which in turn allows the provider to respond with a special “not modified” message (4) if the data has not changed.
Example(s)
The Customer Core microservice of the Lakeside Mutual sample application implements conditional requests in its WebConfiguration
class. Classes annotated with @Configuration
can be used to customize the configuration of the Spring MVC framework. The fingerprint-based variant of Conditional Request 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.
*
* By marking the method as @Bean, Spring can call this
* method and inject the dependency into other components,
* following the inversion of control principle.
* */
@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 to calculate the hashes, or the extra storage used by timestamps and versioning numbers 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 work with stale data.
- How does a Conditional Request count towards a Rate Limit [Zimmermann et al. 2022]?
Be careful when combining Conditional Requests with a Wish List or Wish Template. The data might not have changed, but the client could request different parts of it. In this case, the cached data is unlikely to be sufficient.
Do not mindlessly 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 [Karlton 2009].
The Conditional Request pattern and this refactoring assume that the server is responsible for evaluating the condition. However, it may make sense for the client to evaluate the condition in order to avoid sending a request to the server. For example, a client could consult the HTTP Expires
header to decide whether the data retrieved from the server is still current [Fielding, Nottingham, and Reschke 2022]. This doesn’t guarantee that the client has the latest data, but depending on the use case, that may not be a problem.
Related Content
The online presentation of the Conditional Request pattern coverage presents an example leveraging the Spring framework.
An operation that returns nested data holders that change more or less often than the containing data can prevent this refactoring from being applied. In that case, applying the Extract Information Holder refactoring first to separate the nested data holders from the containing data can help. Chapter 7 of Zimmermann et al. [2022] provides a comprehensive introduction to API quality.
Our catalog includes an Introduce Version Identifier refactoring that focuses on versioning endpoints, not Data Elements.
Conditional requests in Hypertext Transfer Protocol (HTTP/1.1) are defined by RFC 7232 [Fielding and Reschke 2014].
References
Fehling, Christoph, Frank Leymann, Ralph Retter, Walter Schupeck, and Peter Arbitter. 2014. Cloud Computing Patterns: Fundamentals to Design, Build, and Manage Cloud Applications. Springer. https://doi.org/10.1007/978-3-7091-1568-8.
Fielding, Roy T., Mark Nottingham, and Julian Reschke. 2022. “HTTP Caching.” Request for Comments. RFC 9111; RFC Editor. https://doi.org/10.17487/RFC9111.
Fielding, Roy T., and Julian Reschke. 2014. “Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests.” Request for Comments. RFC 7232; RFC Editor. https://doi.org/10.17487/RFC7232.
Karlton, Phil. 2009. “Two Hard Things.” 2009. https://martinfowler.com/bliki/TwoHardThings.html.
Zimmermann, Olaf, Mirko Stocker, Daniel Lübke, Uwe Zdun, and Cesare Pautasso. 2022. Patterns for API Design: Simplifying Integration with Loosely Coupled Message Exchanges. Addison-Wesley Signature Series (Vernon). Addison-Wesley Professional.