18 Batch operations

This chapter covers

  • Batch operations and their differences from executing a series of standard methods
  • Why batch operations should be atomic
  • How batch request methods can hoist common fields to avoid repetition
  • An exploration of each batch standard method (get, delete, create, and update)

This pattern provides guidelines by which users of an API can manipulate multiple resources in bulk without making separate API calls. These so-called batch operations behave similarly to the standard methods (discussed in chapter 7), but they avoid the necessity of multiple back-and-forth interactions of multiple API calls. The end result is a set of methods that permit retrieving, updating, or deleting a collection of resources with a single API call rather than one per resource.

18.1 Motivation

So far, most of the design patterns and guidelines we’ve explored have been focused on interacting with individual resources. In fact, we’ve taken this even further in chapter 8 by exploring how to operate even more narrowly, focusing on addressing individual fields on a single resource. While this has proven quite useful thus far, it leaves a gap on the other end of the spectrum. What if we want to operate more broadly, across multiple resources simultaneously?

In a typical database system, the way we address this is by using transactions. This means that if we want to operate on multiple different rows in a database at the same time, we simply begin a transaction, operate as normal (but within the context of the transaction), and commit the transaction. Depending on the locking functionality of the database and the level of volatility of the underlying data, the transaction may succeed or fail, but the key takeaway here is this atomicity. The operations contained inside the transaction will either all fail or all succeed; there will be no partial success scenario where some operations execute successfully while others don’t.

Unfortunately (or not, depending on who you’re talking to), most web APIs don’t provide this generic transactional functionality—unless the API is for a transactional storage service. The reason for this is quite simple: it’s exceptionally complex to provide this ability. However, this doesn’t take away the need for transaction-like semantics in a web API. One common scenario is when an API user wants to update two separate resources but really needs these two independent API requests to either both succeed or both fail. For example, perhaps we have a ChatRoom logging configuration resource that can be enabled or disabled. We don’t want to enable it before it’s being used, but we don’t want to refer to it as the default logging configuration before it’s enabled. This sort of catch-22 is one of the primary reasons that transactional semantics exist, because without them, our only other option is to try to perform these two independent operations (enable the config and assign it as the default) as close together as possible.

Obviously this is insufficient, but that leads us to the big question: what can we do? How do we get some semblance of atomicity or transactional semantics across multiple API requests without building an entire transaction system like most relational databases have?

18.2 Overview

As much fun as it might be to develop a fully-fledged generic transaction design pattern, that is certainly a bit excessive. So what can we do that gives us the most value for our time? In other words, what corners can we cut to reduce the amount of work needed while still providing support for operating on multiple resources atomically?

In this design pattern, we’ll explore how to provide a limited version of these transactional semantics by specifying several custom methods, analogous to the standard methods we saw in chapter 7, that permit atomic operation on arbitrary groups of resources called batches. These methods will be named quite similarly to the standard methods (e.g., BatchDeleteMessages) but will vary in their implementation depending on the standard method being performed on the batch.

Listing 18.1 Example of a batch Delete method

abstract class ChatRoomApi {
  @post("/{parent=chatRooms/*}/messages:batchDelete")
  BatchDeleteMessages(req: BatchDeleteMessagesRequest): void;
}

As with most proposals like this, it leads to quite a few questions before we can get to work. For example, what HTTP method should we use for each of these methods? Should it always be POST as required for custom methods (see chapter 9)? Or should it match with the HTTP method for each of the standard methods (e.g., GET for BatchGetMessages and DELETE for BatchDeleteMessages)? Or does it depend on the method?

And how extreme do we get with our atomicity requirements? If I attempt to retrieve a batch of Message resources using BatchGetMessages and one of these resources has since been deleted, do I really have to fail the entire operation? Or is it acceptable to skip right past that and return those that did still exist at the time? What about operating on resources across multiple parent resources? In other words, should it be possible to delete Message resources from multiple ChatRooms?

In this pattern, we’ll explore how to define these batch methods, how they work, and all of these curious edge cases that we’ll inevitably run into when building out this functionality in an API.

18.3 Implementation

As we just learned in section 18.2, we will rely on specially named custom methods to support these batch operations. These methods are simply batch versions of the standard methods, with one exception: there is no batch version of the standard list method. This leads to the following batch custom methods:

  • BatchGet<Resources>()

  • BatchCreate<Resources>()

  • BatchUpdate<Resources>()

  • BatchDelete<Resources>()

While we’ll explore the details of each method individually in later sections, it’s certainly worth going through some of the aspects that are common to each, starting with the most important: atomicity.

18.3.1 Atomicity

When we say a set of operations is atomic we mean that these operations are indivisible from one another. So far, all operations we’ve discussed have been themselves considered atomic. After all, you cannot have a standard create method fail partially. The resource is either created or it’s not, but there’s no middle ground. Unfortunately, the way we’ve designed the standard methods to be atomic individually simply doesn’t provide the same guarantee collectively. The goal of these batch operations is to extend that same principle of atomicity to methods performed on multiple resources rather than just a single one.

Meeting this goal means that the action must be atomic even when it might be a bit inconvenient. For example, imagine that we’re trying to retrieve a batch of Message resources using a BatchGetMessages method. If retrieving even one of these resources happens to result in an error, then the entire request must result in an error. This must be the case whether it’s 1 in 5 resulting in an error or 1 in 10,000.

The reason for this is two-fold. First, in many cases (like updating batches of resources), atomicity is the entire point of the operation. We actively want to be sure that either all changes are applied or none of them. While this isn’t exactly the scenario with batch retrieval (we might be okay to get back a null value for items that might not exist), it leads to the second reason. If we have to support partial success, the interface to managing the results can get pretty intricate and complex. Instead, by ensuring either complete success or a single error, the interface is always geared toward complete success and simply returns the list of resources involved in the batch.

As a result, all the batch methods that we’ll explore in this pattern will always be completely atomic. The response messages will be designed such that they can only ever completely succeed or completely fail; there will never be a middle ground.

18.3.2 Operation on the collection

As we learned in section 9.3.2, when it comes to custom methods that interact with collections, we have two different options for the URL format for the method. We can either target the parent resource itself and have the custom method name include the resources it’s interacting with, or we can leave the custom action section as a verb and operate on the collection itself. These two options are summarized in table 18.1.

Table 18.1 Options of targets and corresponding URLs for batch methods

Target

URL

Parent resource

/chatRooms/*:batchUpdateMessages

Collection

/chatRooms/*/messages:batchUpdate

What was pointed out in chapter 9 holds true today. Whenever we’re dealing with a collection where we have multiple resources of the same type, it’s almost always better to operate on the collection itself. This holds true for all of the batch methods we’ll discuss in detail: they’ll all target the collection and therefore have URLs ending in :batch<Method>.

18.3.3 Ordering of results

When we operate on an arbitrary group of resources, we’ll need to send them in a request of sorts. In some cases this might just be a list of unique identifiers (e.g., when retrieving or deleting resources), but in others it will need to be the resources themselves (e.g., when creating or updating resources). In all of these cases though, it’s going to become successively more important that the order of the resources be preserved.

The most obvious example of why it’s important to preserve order is when we’re creating a batch of new resources and not choosing the identifiers ourselves but allowing the service to do so. Since we have no prearranged way of identifying these newly created resources, the simplest way is to use their index in the repeated field of resources that were provided to the request. If we don’t preserve the order, we’ll have to do a deep comparison between the resources we provided and those returned from the batch creation to match up the items created with their server-assigned identifiers.

Listing 18.2 Example code to match request resources

let chatRoom1 = ChatRoom({ title: "Chat 1", description: "Chat 1" });
let chatRoom2 = ChatRoom({ title: "Chat 2", description: "Chat 2"});
 
let results = BatchCreateChatRooms({ resources: [chatRoom1, chatRoom2] });
 
chatRoom1 = results.resources[0];            
chatRoom2 = results.resources[1];
 
for (let resource of results.resources) {
  if (deepCompare(chatRoom1, resource))      
    chatRoom1 = resource;
  else if (deepCompare(chatRoom2, resource))
    chatRoom2 = resource;
  }
}

In the case where we know the order, we can easily associate the results with the requested action.

When we don’t know the order, we have to do a full comparison of the resources for equality of each of their fields (except the ID field) to be sure we have the right resource.

As a result, it’s an important requirement that when a batch method returns the resources involved in a batch they are returned in the same order in which they were provided.

18.3.4 Common fields

When it comes to batch operations, we have two primary strategies to choose from. The first, simpler option is to treat the batch version of a request as just a list of normal, single-resource requests. The second option is a bit more complicated but can reduce duplication by hoisting up the fields that are relevant to the request and simply repeat those.

Listing 18.3 Strategies of hoisting fields and relying on repeated requests

interface GetMessageRequest {
  id: string;
}
 
interface BatchGetMessagesRequest {
  requests: GetMessageRequest[];     
}
 
interface BatchGetMessagesRequest {
  ids: string[];                     
}

Here we simply have a list of GetMessageRequest interfaces.

Here we hoist out the relevant field (id) and embed it as a list of IDs for a batch.

It turns out that we’ll need to rely on both strategies, depending on the method in question, because the simpler strategy (providing a list of requests) is a much better fit for cases where we need quite a bit of customization (e.g., if we need to create resources that might have different parents), while the strategy of hoisting fields out of the request is a much better fit for simpler actions such as retrieving or deleting resources, as no additional information is necessary.

Additionally, these strategies are not necessarily mutually exclusive. In some cases, we will actually hoist some values while still relying on the strategy of using a list of requests to communicate the important information. A good example of this is updating a batch of resources, where we will rely on a repeated list of update requests but also hoist the parent field and potentially the field mask to handle partial batch updates.

While this combination of strategies sounds great, it leads to an important question: what happens when a hoisted field is different from the field set on any of the individual requests? In other words, what if we have a hoisted parent field set to ChatRoom 1 and one of the to-be-created resources has its parent field set to ChatRoom 2? The short answer keeps with the philosophy of fast and nonpartial failure: the API should simply throw an error and reject the request. While it might be tempting to treat the resource’s redefinition of the hoisted field as a more specific override, it’s quite possible that there is a semantic mistake in the code here, and operating on groups of resources is certainly not the time to try to make inferences about user intentions. Instead, if a user intends to vary the hoisted field across multiple resources, they should leave the hoisted field blank or set it to a wild card value, which we’ll discuss in the next section.

18.3.5 Operating across parents

One of the most common reasons for relying on a list of individual requests rather than hoisting fields upward into the batch request is to operate on multiple resources that might also belong to multiple different parents. For example, listing 18.4 shows one option to define an example BatchCreateMessages method, which supports creating resources across multiple different parents, but doing so in an unusual (and incorrect) way.

Listing 18.4 Incorrect way of supporting multi-parent creation in a batch method

interface Message {
  id: string;
  title: string;
  description: string;
}
 
interface CreateMessageRequest {
  parent: string;
  resource: Message;
}
 
interface BatchCreateMessageRequest {
  parents: string[];                   
  resources: Message[];                
}

We need a list of the parents to which these Message resources will belong.

Here we’ve hoisted the Message resources from the CreateMessageRequest interface.

As you can see, it relies on a list of the resources to be created, but we also need to know the parent of each of these. And since that field doesn’t exist on the resource itself, we now need a way of keeping track of each resource’s parent. Here, we’ve done this by having a second list of parents. While this does technically work, it can be quite clumsy to work with due to the strict dependence on maintaining order in the two lists. How do we handle this cross-parent batch operation?

The simple answer is to rely on a wild card value for the parent and allow the parent itself to vary inside the list of requests. In this case, we’ll standardize the hyphen character (-) as the wild card to indicate “across multiple parents.”

Listing 18.5 HTTP request to create two resources with different parents

POST /chatRooms/-/messages:batchCreate HTTP/1.1   
Content-Type: application/json
 
{
  "requests": [
    {
      "parent": "chatRooms/1",                    
      "resource": { ... }
    },
    {
      "parent": "chatRooms/2",                    
      "resource": { ... }
    }
  ]
}

We rely on a wild card hyphen character to indicate multiple parents, to be defined in the requests.

We define the actual parent in the list of requests to create the resources.

This same mechanism should be used for all the other batch methods that might need to operate across resources belonging to multiple parents. However, just like with the hoisted fields, this raises a question about conflicting values: what if a single parent is specified in the URL, yet some of the request-level parent fields conflict? What should be done?

In this case, the API should behave exactly as defined in section 18.3.4: if there is a conflict, we should reject the request as invalid and await clarification from the user in the form of another API call. This is for exactly the same reasons as noted before: the user’s intent is unclear, and operating on a (potentially large) group of resources is certainly not the time to guess what those intentions might be.

With that, we can finally get to the good part: defining exactly what these methods look like and how each of them should work. Let’s start by looking at one of the simplest: batch get.

18.3.6 Batch Get

The goal of the batch get method is to retrieve a set of resources from an API by providing a group of unique identifiers. Since this is analogous to the standard get method, and is idempotent, batch get will rely on the HTTP GET method for transport. This, however, presents a bit of a problem in that GET methods generally do not have a request body. The result is that the list of identifiers is provided in the query string like we saw with field masks in chapter 8.

Listing 18.6 Example of the batch get method

abstract class ChatRoomApi {
  @get("/chatrooms:batchGet")
  BatchGetChatRooms(req: BatchGetChatRoomsRequest):
    BatchGetChatRoomsResponse;
 
  @get("/{parent=chatRooms/*}/messages:batchGet")
  BatchGetMessages(req: BatchGetMessagesRequest):
    BatchGetMessagesResponse;
}
 
interface BatchGetChatRoomsRequest {     
  ids: string[];                         
} 
 
interface BatchGetChatRoomsResponse {
  resources: ChatRoom[];                 
}
 
interface BatchGetMessagesRequest {
  parent: string;                        
  ids: string[];                         
}
interface BatchGetMessagesResponse {
  resources: Message[];                  
}

Since ChatRoom resources have no parent, the batch get request doesn’t provide a place to specify a parent.

All batch get requests specify a list of IDs to retrieve.

The response always includes a list of resources, in the exact same order as the IDs that were requested.

Since Message resources have parents, the request has a place to specify the parent (or provide a wild card).

As you can see, unless the group of resources are top-level resources without a parent, we’ll need to specify something for the parent value in the request message. If we intend to limit the retrievals to be all from the same parent, then we have an obvious answer: use the parent identifier itself. If, instead, we want to provide the ability to retrieve resources across multiple different parents, we should rely on a hyphen character as a wild card value, as we saw in section 18.3.5.

Although it might be tempting to always permit cross-parent retrievals, think carefully about whether it makes sense for the use case. All the data might be in a single database now, but in the future the storage system may expand and distribute data to many different places. Since a parent resource is an obvious choice for a distribution key, and the distributed storage becomes expensive or difficult to query outside the scope of a single distribution key, providing the ability to retrieve resources across parents could become exceptionally expensive or impossible to query. In those cases, the only option will be to disallow the wildcard character for a parent, which is almost certainly a breaking change (see chapter 24). And remember, as we saw in section 18.3.5, if an ID provided in the list would conflict with an explicitly specified parent (i.e., any parent value that isn’t a wildcard), the request should be rejected as invalid.

Next, as we learned in section 18.3.1, it’s critical that this method be completely atomic, even when it’s inconvenient. This means that even when just 1 out of 100 IDs is invalid for any reason (e.g., it doesn’t exist or the requesting user doesn’t have access to it), it must fail completely. In cases where you’re looking for less strict guarantees, it’s far better to rely on the standard list method with a filter applied to match on one of a set of possible identifiers (see chapter 22 for more information on this).

Additionally, as we learned in section 18.3.4, if we want to support partial retrieval of the resources, such as requesting only a single field from each of the IDs specified, we can hoist the field mask just as we did with the parent field. This single field mask should be applied across all resources retrieved. This should not be a list of field masks in order to apply a different field mask to each of the resources specified.

Listing 18.7 Adding support for partial retrieval in a batch get request

interface BatchGetMessagesRequest {
  parent: string;
  ids: string[];
  fieldMask: FieldMask;    
}

To enable partial retrievals, the batch request should include a single field mask to be applied to all retrieved resources.

Finally, it’s worth noting that despite the fact that the response to this request could become quite large, batch methods should not implement pagination (see chapter 21). Instead, batch methods should define and document an upper limit on the number of resources that may be retrieved. This limit can vary depending on the size of each individual resource, but generally should be chosen to minimize the possibility of excessively large responses that can cause performance degradation on the API server or unwieldy response sizes for the API clients.

18.3.7 Batch Delete

In a similar manner to the batch get operation, batch delete also operates on a list of identifiers, though with a very different end goal. Rather than retrieving all of these resources, its job is to delete each and every one of them. This operation will rely on an HTTP POST method, adhering to the guidelines for custom methods that we explored in chapter 9. And since the request must be atomic, deleting all the resources listed or returning an error, the final return type is void, representing an empty result.

Listing 18.8 Example of the batch delete method

abstract class ChatRoomApi {
  @post("/chatrooms:batchDelete")
  BatchDeleteChatRooms(req: BatchDeleteChatRoomsRequest): void;
 
  @post("/{parent=chatRooms/*}/messages:batchDelete")
  BatchDeleteMessages(req: BatchDeleteMessagesRequest): void;
}
 
interface BatchDeleteChatRoomsRequest {  
                                         
  ids: string[];                         
} 
 
interface BatchDeleteMessagesRequest {
  parent: string;                        
  ids: string[];                         
}

For top-level resources, no parent field is provided on requests.

Just like batch get, batch delete operations accept a list of identifiers rather than any resources themselves.

Since Message resources are not top-level, we need a field to hold that parent value.

In most aspects, this method is quite similar to the batch get method we just went through in section 18.3.6. For example, top-level resources might not have a parent field, but others do. And when the parent field is specified, it must match the parent of the resources whose IDs are provided or return an error result. Further, just as is the case for batch get operations, deleting across several different parents is possible by using a wild card character for the parent field; however, it should be considered carefully due to the potential issues down the line with distributed storage systems.

One area worth calling out specifically again is that of atomicity: batch delete operations must either delete all the resources listed or fail entirely. This means that if 1 resource out of 100 is unable to be deleted for any reason, the entire operation must fail. This includes the case where the resource has already been deleted and no longer exists, which was the actual intent of the operation in the first place. The reason for this, explored in more detail in chapter 7, is that we need to take an imperative view of the delete operation rather than a declarative one. This means that we must be able to say not just that the resource we wanted deleted indeed no longer exists, but that it no longer exists due to the execution of this specific operation and not due to some other operation at an earlier point in time.

Now that we’ve covered the ID-based operations, let’s get into the more complicated scenarios, starting with batch create operations.

18.3.8 Batch Create

As with all the other batch operations, the purpose of a batch create operation is to create several resources that previously didn’t exist before—and to do so atomically. Unlike the simpler methods we’ve explored already (batch get and batch delete) batch create will rely on a more complicated strategy of accepting a list of standard create requests alongside a hoisted parent field.

Listing 18.9 Example of the batch create method

abstract class ChatRoomApi {
  @post("/chatrooms:batchCreate")
  BatchCreateChatRooms(req: BatchCreateChatRoomsRequest):
    BatchCreateChatRoomsResponse;
 
  @post("/{parent=chatRooms/*}/messages:batchCreate")
  BatchCreateMessages(req: BatchCreateMessagesRequest):
    BatchCreateMessagesResponse;
}
 
interface CreateChatRoomRequest {
  resource: ChatRoom;
}
 
interface CreateMessageRequest {
  parent: string;
  resource: Message;
}
 
interface BatchCreateChatRoomsRequest {
  requests: CreateChatRoomRequest[];     
}
 
interface BatchCreateMessagesRequest {
  parent: string;                        
  requests: CreateMessageRequest[];          
}
 
interface BatchCreateChatRoomsResponse {
  resources: ChatRoom[];
}
 
interface BatchCreateMessagesResponse {
  resources: Message[];
}

Rather than a list of resources or IDs, we rely on a list of standard create requests.

When the resource is not top-level, we also include the parent (which may be a wild card).

Similar to the other batch methods, the parent fields are only relevant for non-top-level resources (in this case, the ChatRoom resource) and the restrictions remain the same (that if a non-wild card parent is provided, it must be consistent with all the resources being created). The primary difference is in the form of the incoming data. In this case, rather than pulling the fields out of the standard create request and directly into the batch create request, we simply include the standard create requests directly.

This might seem unusual, but there’s a pretty important reason and it has to do with the parent. In short, we want to provide the ability to create multiple resources atomically that might have several different parents. Unfortunately, the parent of a resource isn’t typically stored as a field directly on the resource itself when it’s being created. Instead, it’s often provided in the standard create request, resulting in an identifier that includes the parent resource as the root (e.g., chatRooms/1/messages/2). This restriction means that if we want to allow cross-parent resource creation without requiring client-generated identifiers, we need to provide both the resource itself along with its intended parent, which is exactly what the standard create request interface does.

Another similarity to the other batch methods is the ordering constraint. Just like other methods, the list of newly created resources absolutely must be in the exact same order as they were provided in the batch request. While this is important in most other batch methods, it is even more so in the case of batch create because, unlike the other cases, the identifiers for the resources being created may not yet exist, meaning that it can be quite difficult to discover the new identifiers for the resources that were created if they are returned in a different order than the one in which they were sent.

Finally, let’s wrap up by looking at the batch update method, which is quite similar to the batch create method.

18.3.9 Batch Update

The goal of the batch update method is to modify a group of resources together in one atomic operation. And just as we saw with the batch create method, batch update will rely on a list of standard update requests as the input. The difference in this case is that the rationale for needing a list of requests rather than a list of resources should be quite obvious: partial updates.

In the case of updating a resource, we might want to control the specific fields to be updated, and it’s quite common that different fields might need to be updated on different resources in the batch. As a result, we can treat the field mask similarly to how we treated the parent field when designing the batch create method. This means that we want the ability to apply different field masks to different resources being updated, but also the ability to apply a single blanket field mask across all the resources being updated. Obviously these must not conflict, meaning that if the batch request has a field mask set, the field masks on individual requests must either match or be left blank.

Listing 18.10 Example of the batch update method

abstract class ChatRoomApi {
  @post("/chatrooms:batchUpdate")                              
  BatchUpdateChatRooms(req: BatchUpdateChatRoomsRequest):
    BatchUpdateChatRoomsResponse;
 
  @post("/{parent=chatRooms/*}/messages:batchUpdate")          
  BatchUpdateMessages(req: BatchUpdateMessagesRequest):
    BatchUpdateMessagesResponse;
}
 
interface UpdateChatRoomRequest {
  resource: ChatRoom;
  fieldMask: FieldMask;
}
 
interface UpdateMessageRequest {
  resource: Message;
  fieldMask: FieldMask;
}
 
interface BatchUpdateChatRoomsRequest {
  requests: UpdateChatRoomRequest[];                       
  fieldMask: FieldMask;                                    
}
 
interface BatchUpdateMessagesRequest {
  parent: string;                                          
  requests: UpdateMessageRequest[];                        
  fieldMask: FieldMask;   
}
 
interface BatchUpdateChatRoomsResponse {
  resources: ChatRoom[];
}
 
interface BatchUpdateMessagesResponse {
  resources: Message[];
}

Despite the standard update method using the HTTP PATCH method, we rely on the HTTP POST method.

Just like the batch create method, we use a list of requests rather than a list of resources.

We can hoist the field mask for partial updates into the batch request if it’s the same across all resources.

It’s also worth mentioning that even though we use the HTTP PATCH method in the standard update method, to avoid any potential conflicts (and from breaking any standards) the batch update method should use the HTTP POST method as is done for all other custom methods.

18.3.10 Final API definition

With that, we can wrap up by looking at the final API definition for supporting batch methods in our example chat room API. Listing 18.11 shows how we might put all of these batch methods together to support a wide range of functionality, operating on potentially large groups of resources with single API calls.

Listing 18.11 Final API definition

abstract class ChatRoomApi {
  @post("/chatrooms:batchCreate")
  BatchCreateChatRooms(req: BatchCreateChatRoomsRequest):
    BatchCreateChatRoomsResponse;

  @post("/{parent=chatRooms/*}/messages:batchCreate")
  BatchCreateMessages(req: BatchCreateMessagesRequest):
    BatchCreateMessagesResponse;
 
  @get("/chatrooms:batchGet")
  BatchGetChatRooms(req: BatchGetChatRoomsRequest):
    BatchGetChatRoomsResponse;

  @get("/{parent=chatRooms/*}/messages:batchGet")
  BatchGetMessages(req: BatchGetMessagesRequest):
    BatchGetMessagesResponse;

  @post("/chatrooms:batchUpdate")
  BatchUpdateChatRooms(req: BatchUpdateChatRoomsRequest):
    BatchUpdateChatRoomsResponse;

  @post("/{parent=chatRooms/*}/messages:batchUpdate")
  BatchUpdateMessages(req: BatchUpdateMessagesRequest):
    BatchUpdateMessagesResponse;
 
  @post("/chatrooms:batchDelete")
  BatchDeleteChatRooms(req: BatchDeleteChatRoomsRequest): void;
 
  @post("/{parent=chatRooms/*}/messages:batchDelete")
  BatchDeleteMessages(req: BatchDeleteMessagesRequest): void;
}
 
interface CreateChatRoomRequest {
  resource: ChatRoom;
}
 
interface CreateMessageRequest {
  parent: string;
  resource: Message;
}
 
interface BatchCreateChatRoomsRequest {
  requests: CreateChatRoomRequest[];
}
 
interface BatchCreateMessagesRequest {
  parent: string;
  requests: CreateMessageRequest[];
}
 
interface BatchCreateChatRoomsResponse {
  resources: ChatRoom[];
}
interface BatchCreateMessagesResponse {
  resources: Message[];
}
 
interface BatchGetChatRoomsRequest {
  ids: string[];
}
 
interface BatchGetChatRoomsResponse {
  resources: ChatRoom[];
}
 
interface BatchGetMessagesRequest {
  parent: string;
  ids: string[];
}
 
interface BatchGetMessagesResponse {
  resources: Message[];
}
 
interface UpdateChatRoomRequest {
  resource: ChatRoom;
  fieldMask: FieldMask;
}
 
interface UpdateMessageRequest {
  resource: Message;
  fieldMask: FieldMask;
}
 
interface BatchUpdateChatRoomsRequest {
  parent: string;
  requests: UpdateChatRoomRequest[];
  fieldMask: FieldMask;
}
 
interface BatchUpdateMessagesRequest {
  requests: UpdateMessageRequest[];
  fieldMask: FieldMask;
}
 
interface BatchUpdateChatRoomsResponse {
  resources: ChatRoom[];
}
 
interface BatchUpdateMessagesResponse {
  resources: Message[];
}
 
interface BatchDeleteChatRoomsRequest {
  ids: string[];
}
 
interface BatchDeleteMessagesRequest {
  parent: string;
  ids: string[];
}

18.4 Trade-offs

For many of these different batch methods, we made quite a few very specific decisions that have some pretty important effects on the resulting API behavior. First, all of these methods put atomicity above all else, even when it might be a bit inconvenient. For example, if we attempt to delete multiple resources and one of them is already deleted, then the entire batch delete method will fail. This was an important trade-off to avoid dealing with an API that supports returning results that show some pieces succeeding and some failing, and instead focus on sticking to the behavior of transactional semantics in most modern database systems. While this might result in annoyances for common scenarios, it ensures that the API methods are as consistent and simple as possible while still providing an important bit of functionality.

Next, while there may be some inconsistencies in the format of the input data (sometimes its raw IDs; other times it’s the standard request interfaces), the design of these methods emphasizes simplicity over consistency. For example, we could stick to a repeated list of standard requests for all batch methods; however, some of these methods (in particular, batch get and batch delete) would contain a needless level of indirection just to provide the identifiers. By hoisting these values into the batch request, we get a simpler experience at the cost of a reasonable amount of inconsistency.

18.5 Exercises

  1. Why is it important for results of batch methods to be in a specific order? What happens if they’re out of order?

  2. In a batch update request, what should the response be if the parent field on the request doesn’t match the parent field on one of the resources being updated?

  3. Why is it important for batch requests to be atomic? What would the API definition look like if some requests could succeed while others could fail?

  4. Why does the batch delete method rely on the HTTP POST verb rather than the HTTP DELETE verb?

Summary

  • Batch methods should be named according to the format of Batch<Method> <Resources>() and should be completely atomic, performing all operations in the batch or none of them.

  • Batch methods that operate on multiple resources of the same type should generally target the collection rather than a parent resource (e.g., POST /chatRooms/1234/messages:batchUpdate rather than POST /chatRooms/1234:batchUpdateMessages).

  • Results from batch operations should be in the same order as the resources or requests were originally sent.

  • Use a wild card hyphen character to indicate multiple parents of a resource to be defined in the individual requests.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.116.183