REST: What to return when a request changes state of more than one entity?

Viewed 364

REST service has two nodes

GET /items returns list of Items:

[
  {
    "id": 34,
    "name": "apple",
    "available_amount" : 10
  },
  {
    "id": 37,
    "name": "banana",
    "available_amount" : 30
  },
  ...
]

POST /orders creates order with requested items (assume we order 6 apples here) and returns the order:

{
  "id": 12337,
  "items": [
    {
      "id": 34,
      "name": "apple",
      "ordered_amount": 6
    }    
  ]
}

How do I inform the client application about the created order which also reduced the available_amount to 4 since 6 apples have been sold.

Do I return the affected resources with the order result, so the client knows what to update?

Like with nodes path:

{
  "id": 12337,
  "items": [
    {
      "item_id": 34,
      "ordered_amount": 6
    }
  ],
  "affected_resources" : [
    "/items/34"
  ]
}

or even the fully changed resource:

{
  "id": 12337,
  "items": [
    {
      "item_id": 34,
      "ordered_amount": 6
    }
  ],
  "affected_resources": {
    "items": [
      {
        "id": 34,
        "name": "apple",
        "available_amount": 4
      }
    ]
  }
}

or something completely different?

What is considered best practice in these cases?

3 Answers

Your request was a POST that creates a single new Order resource.

While it is relevant to the client that things may happen to other resources, that is only true if those resources are within the same bounded context. Inventory is clearly a different bounded context.

At a minimum send only what you can claim is correct at that time, which is the ID of the new order or a representation of the new order. There could be more to the story though...

At 1000 orders per second, you could send invalid data about your inventory.

Now, if informing the client that their order of apples and bananas was indeed an "order of apples and bananas" then you can safely tell your client that; that's a nice feature because you're transferring the state the client sent to you back to the client, and it's the truth. You can even tell the client how many of each they ordered. Still truth. Saying you have N apples and M bananas left... well that might be a lie.

While we are talking about resources here, which are not the same thing as entities in the DDD sense, there's a close enough approximation. Resources are often (but not always) representations of DDD entities (and by extension aggregates). Your API usually is going to operate against a single bounded context, so introducing concepts like inventory is clearly not in the ordering bounded context.

If you want to make inventory a thing of interest to clients, that's a separate API.

A Short Story

Two customers place orders. Your API dutifully takes their orders.

Customer A is a retail customer and just wants their stuff. You inform them you got their order (A book on REST, and a cloud subscription, right?) and the customer sees that and the order tracking link, and starts requesting order status. "Where's my stuff?"

Customer B is a wholesaler and asks for 1000 of those REST books that it wants to have on hand to fulfill when they resell them. You send them their order, which also contains links to the book inventory. Customer B is glad because those books sell quickly, and needs to ask you (via your separate API of course) whether you have 500 more. Your other API checks inventory and says "I have 500 I can give you right now, but that's only good for the next 15 minutes..."

In short, I'd say return anything from the minimum amount of information to indicate the POST was successful (an ID and a 201 response code) to the maximum (everything about the order that was true when the client sent it, plus anything related that is also guaranteed to be true from within the bounded context).

The ideal for APIs are to communicate representations of resources to the client that the client can reasonably act on with success. This includes data about resources and data representing actions the client can take.

While it's a bit beyond your question, HATEOS provides a lot of guidance in this area (blog post).

There's a couple of answers to this, I'll share how we do this.

Entities are addressable by a URI, so we want a request on entity A to have an effect on entity B, with a different uri.

A POST request on /orders could tell a client that /items is now expired, using an invalidates link:

Link: </items>; rel=invalidates

Our client listens for these links and automatically expels things from it's cache when they pop up. This client also lets you listen for these 'stale' events and automatically refresh.

itemsResource.on('stale' => itemsResource.refresh());

Another way we handle this is via a Websocket. The cases we use websockets, we also emit 'stale' events for specific uris. The benefit of this approach is that if another user adds items to an order (and reduces the inventory), this will also work.

A third option could be use something like HAL. The HTTP response to your POST request could:

  1. Return the full representation of the order
  2. Include a Content-Location header indicating that response body matches the representation.
  3. Then you could use HAL's _embedded feature to send along resource stats of other related resources.

All of this requires a relatively smart client. We've built "Ketting" that handles a lot of these cases, but similar designs could be used for other languages.

Best practice is to return success/failure for the order creation request, optionally with a success or error message. The client is informed of the created (or not) order based on that return status. Any new inventory GET request would be done by the client when deemed appropriate. That is to say, the Application State should be maintained on the client; a REST API should use a stateless protocol.

I'm not sure what you're programming, but whether it's all or nothing or you can partially fulfill orders, it would be wise to return a success and failure response for each item id. In this case, the overall response would be success if the format of the order was valid (and failure if not), and then an array of responses including item_id, amount_ordered, amount_fulfilled. In the instance when it's all or nothing, this would presumably invoke a second stage of ordering, where you inform the user that such-and-such has limited stock or is sold out and would they like to adjust their order or cancel it.

To update inventory, if you allow the client to pass in the items of interest as a parameter in the inventory/items GET request, you could program the client to ask for an inventory update based on the item list it used to create the order in the POST request. This would be an example of the client maintaining its application state, i.e. "items ordered in the last order that will require new inventory checks."

Related