How to Shape Responses to API Calls
Modern software systems are distributed systems. Mobile and Web clients exchange information with backend API services, often hosted by a single or even multiple cloud providers, and various backends trigger activities in each other. Independent of the technologies and protocols used, request and response messages travel through one or several API clients and their service providers in such systems. In another article, we introduced the notion of API quality and discussed what the right service granularity in API designs is.
In this article, we are specifically interested in the following question, also referred to as response shaping:
How to avoid unnecessary data transfer in API and message design?
Many solutions exist to optimize API calls concerning lean messaging (aka data parsimony). Here, we discuss widely used ones in the form of five API design patterns, PAGINATION, WISH LIST, WISH TEMPLATE, CONDITIONAL REQUEST, and REQUEST BUNDLE.
For more details on these patterns (and many more), please consult our book Patterns for API Design: Simplifying Integration with Loosely Coupled Message Exchanges.
Option 1: Pagination
PAGINATION is a widely used option to avoid unnecessary data transfer in an API. Sometimes complex data elements can contain large amounts of repetitive data (for instance, data records). If an API client only requires a subset of this information at a time, it might be better to send the information in small chunks rather than in one large transmission.
For example, consider hundreds of person records being contained in the data, but a client displaying the information page by page (with 20 records per page) and requiring user input to advance to the next page. Showing only the current page, and maybe prefetching one or two pages in either stepping direction, might be far more efficient in terms of performance and bandwidth use than downloading all available records before even starting to display the data. In such design situations, you can consider the PAGINATION pattern.
Pattern: PAGINATION | |
Problem | How can an API provider deliver large sequences of structured data without overwhelming clients? |
Solution | Divide large response data sets into manageable and easy-to-transmit chunks (also known as pages). Send one chunk of partial results per response message, and inform the client about the total and remaining number of chunks. Provide optional filtering capabilities to allow clients to request a particular selection of results. For extra convenience, include a reference to the next page from the current one. |
The message exchanges resulting from applying PAGINATION are as follows (three pairs of requests and replies are shown in the figure, as well as some of the required control metadata:
Click to view full-sized image
The pattern comes in variants such as offset-based pagination, cursor-based pagination, and time-based pagination. The general idea is easy to understand; that said, implementing the pattern is usually harder than it seems. Additional design concerns include where, when, and how to define the page size, how to order results, where and how to store intermediate results, how long to store it (deletion policy, timeouts), how to deal with request repetition, and how to correlate pages with the initial, previous, and next requests.
Options 2 and 3: Wish List, Wish Template
API providers often serve diverse clients. It can be hard to design API operations that provide precisely the data required by all these clients. Some of them might use only a subset of the data offered by the operations; other clients might expect more data. The information need might not be predictable before runtime. A possible way to solve this problem is to let the client inform the provider at runtime about its data fetching preferences. A simple option to do this is to let the client send a list of its desires.
Pattern: WISH LIST | |
Problem | How can an API client inform the API provider at runtime about the data it is interested in? |
Solution | As an API client, provide a WISH LIST in the request that enumerates all desired data elements of the requested resource. As an API provider, deliver only those data elements in the response message that are enumerated in the Wish List. |
In an example case, a request for customer data might return all of the available attributes:
curl -X GET http://localhost:8080/customers/gktlipwhjr
For customer ID gktlipwhjr, this would return the following:
{
"customerId": "gktlipwhjr",
"firstname": "Max",
"lastname": "Mustermann",
"birthday": "1989-12-31T23:00:00.000+0000",
"streetAddress": "Oberseestrasse 10",
"postalCode": "8640",
"city": "Rapperswil",
"email": "admin@example.com",
"phoneNumber": "055 222 4111",
"moveHistory": [ ],
"customerInteractionLog": {
"contactHistory": [ ],
"classification": {
"priority": "gold"
}
}
}
To improve this design, a WISH LIST in the query string can restrict the result to the fields included in the wish. In the example, an API client might be interested in only the customerId, birthday, and postalCode:
curl -X GET http://localhost:8080/customers/gktlipwhjr?fields=customerId,birthday,postalCode
The returned response now contains only the requested fields:
{
"customerId": "gktlipwhjr",
"birthday": "1989-12-31T23:00:00.000+0000",
"postalCode": "8640"
}
This response is much smaller; only the information required by the client is transmitted.
A simple list of client wishes is not always easy to specify, for example, if a client requests only specific fractions of deeply nested or repetitive parameter structures. An alternative solution that works better for complex parameters is to let the client send a template expressing the wishes as examples in its request:
Pattern: WISH TEMPLATE | |
Problem | How can an API client inform the API provider about nested data that it is interested in? How can such preferences be expressed flexibly and dynamically? |
Solution | Add one or more additional parameters to the request message that mirror the parameters in the corresponding response message. Make these parameters optional, or use Boolean as their types so that their values indicate whether a parameter should be included. |
The following service contract introduces a WISH TEMPLATE, indicated by a <
:
data type PersonalData P // placeholder
data type Address P // placeholder
data type CustomerEntity <> {PersonalData?, Address?}
endpoint type CustomerInformationHolderService
exposes
operation getCustomerAttributes
expecting payload {
"customerId":ID, // the customer ID
<>"mirrorObject":CustomerEntity
// has same structure as desired result set
}
delivering payload CustomerEntity
In this example of an API, the client can send a CustomerEntity template that may include PersonalData and Address attributes (this is defined in the data type definition CustomerEntity). The provider can then check which attributes were sent and respond with a filled-out CustomerEntity instance.
The API contract in the example is specified in Microservice Domain-Specific Language (MDSL). This DSL allows API designers to specify (micro-)service contracts, their data representations and API endpoints. In support of API sketching, MDSL abstracts from technology-specific interface description languages such as OpenAPI/Swagger, GraphQL, and Protocol Buffers, but provides tool-supported bindings to them.
Options 4 and 5: Conditional Request, Request Bundle
Let us consider another situation in which an analysis of the usage of the operations of an API provider shows that some clients keep requesting the same server-side data. The requested data changes much less frequently than the clients send their requests. In such cases, we can avoid unnecessary data transfers with CONDITIONAL REQUESTs.
Pattern: CONDITIONAL REQUEST | |
Problem | How can unnecessary server-side processing and bandwidth usage be avoided when frequently invoking API operations that return rarely changing data? |
Solution | Make requests conditional by adding METADATA ELEMENTS to their message representations (or protocol headers) and processing these requests only if the condition specified by the metadata is met. |
For example, the provider could supply a fingerprint for each resource accessed, which the client caches locally along with the data. This fingerprint can then be included in a subsequent request to indicate which "version" of the data it already so that only newer versions are sent. The following figure illustrates the solution elements:
Click to view full-sized image
Different conditions and types of fingerprint exist, for example, version numbers, hash codes or timestamps are variants and applications of the pattern.
In other situations, an analysis of the usage of the already-deployed API might reveal that clients are issuing many similar, but independent requests for which individual responses are returned. These batches of requests may hurt scalability and throughput. In such situations, the REQUEST BUNDLE pattern is eligible.
Pattern: REQUEST BUNDLE | |
Problem | How can the number of requests and responses be reduced to increase communication efficiency? |
Solution | Define a REQUEST BUNDLE as a data container that assembles multiple independent requests in a single request message. Add metadata such as identifiers of individual requests and bundle element counters. |
In the Customer Core service of Lakeside Mutual (a case study running through the book, implemented in Java and Spring Boot and openly available on GitHub), clients can request multiple customers from the customer's INFORMATION HOLDER RESOURCE by specifying an ATOMIC PARAMETER LIST of customer ID ELEMENTS. A path parameter serves as a bundle container. A comma (,) separates the bundle elements:
curl -X GET http://localhost:8080/customers/ce4btlyluu,rgpp0wkpec
This will return the two requested customers as DATA ELEMENTS, represented as JSON objects in a bundle-level container array called customers:
{
"customers": [
{
"customerId": "ce4btlyluu",
"firstname": "Robbie",
"lastname": "Davenhall",
"birthday": "1961-08-11T23:00:00.000+0000",
...
"_links": { ... }
},
{
"customerId": "rgpp0wkpec",
"firstname": "Max",
"lastname": "Mustermann",
"birthday": "1989-12-31T23:00:00.000+0000",
...
"_links": { ... }
}
],
"_links": { ... }
}
REQUEST BUNDLE can be seen as an extension of the general "Command" design pattern: each individual request is a command according to terminology from Design Patterns.
Decision Drivers and Tradeoffs
When deciding on one or more of the response shaping patterns, a primary decision driver concerns the individual information needs of clients, which have to be analyzed to find out which of the patterns are applicable and promise to have enough benefits. The currently trending GraphQL technology can be seen as an extreme form of declarative WISH TEMPLATE.
Consider situations where data transfer over the network is perceived as a potential bottleneck. In such cases, data parsimony can drive the decision. Data parsimony is an essential general design principle in distributed systems, and the five patterns can help achieve parsimonious ways of data transmission.
Security can be a driver not to apply the patterns WISH LIST and WISH TEMPLATE. Enabling clients to provide options regarding which data to receive may unwittingly expose sensitive data to unexpected requests or open up additional attack vectors. For instance, sending long data element lists or using invalid attribute names might introduce an API-specific form of denial-of-service attack. Data that is not transferred cannot be stolen and cannot be tampered with.
Finally, enhancing and, as a result, complicating an API—as all five patterns do to some extent—increases the complexity of API client and provider programming. Exceptional invocation cases introduced by the patterns require more testing and maintenance efforts.
In conclusion, no data transfer reduction might be possible or required for certain API operations. If it makes sense, however, unnecessary data transfer can be avoided through one of the two patterns WISH LIST and WISH TEMPLATE. Both of these patterns inform the provider about required data at runtime. Other alternatives are CONDITIONAL REQUESTS to avoid repeated responses to the exact requests and REQUEST BUNDLES to aggregate multiple requests in a single message. These patterns can be combined with either WISH LIST or WISH TEMPLATE. The combination of CONDITIONAL REQUEST with a WISH LIST or a WISH TEMPLATE is helpful for indicating which subset of resources is requested in case the condition evaluation states that the resource should be sent again. REQUEST BUNDLE can, in principle, be combined with each of the prior alternatives such as CONDITIONAL REQUEST, WISH LIST, or WISH TEMPLATE.
Architectural Decisions and API Patterns in our Book
Patterns for API Design: Simplifying Integration with Loosely Coupled Message Exchanges features these five patterns and 39 more. These patterns are organized into themes and categories, which is shown in the following figure:
The book features an introduction to API fundamentals, a domain model for APIs, and a decision model identifying pattern selection questions, options, and criteria; six narratives with 29 recurring decisions guide through the conceptual level of API design. The book also applies the patterns to three sample cases. A cheat sheet suggesting patterns by design issue and/or design smell is included as well.
THIS KNOWLEDGE IS FROM THE BOOK
No comments: