Search code examples
restarchitectureidempotenthypermedia

Implementing idempotency on the server using hypermedia in REST API


Suppose I'm creating a REST API and my POST method creates a new Order. And I'd like to use Hypermedia in my API, supplying a list of available actions to the client, and path to a POST method should contain IdempotencyKey (so IdempotencyKey is created on the server, not on the client). For example:

{
  // ...
  "actions": [
    {
      "name": "createOrder",
      "path": "/api/v1/orders/create/<idempotency-key-uuid>",
      "method": "POST"
    }
  ]
}
  1. How would you actually get this initial response if a user lands right on to the "Create Order" page? Should I issue some kind of get request when the page is initially loaded (to get the path of "createOrder" action)?

  2. What happens when user clicks on "Create" button, so the POST request is issued, and then something bad happened (lost connection, timeout of some kind). The client does not get any response but the new Order is still created. The user refreshes the page to try again (to create Order). We should get the new IdempotencyKey if the order has been created, and the same IdempotencyKey if it hasn't, right? Is there any way to generate the same or the new IdempotencyKey on the server depending on what actually happened?

Looks like it must depend on the state of the server, but I can't really figure out what the right implementation would be.


Solution

  • I've never come across the idea of having an IdempotencyKey specifically - assuming it's distinct from an ObjectKey / UniqueID / EntityKey.

    If an object's uniqueness is controlled by the server then typically a client would simply call /api/v1/orders/create so that "the server" can do its thing. including generating a new unique ID, which the response might include in the metadata.

    How would you actually get this initial response if a user lands right on to the "Create Order" page?

    Dunno - sounds complicated and brittle. I'd need to know why such an approach was needed over the more usual approach I described above. I'm assuming the goal is to avoid creating duplicate Objects if two very similar 'create' requests get sent by accident?

    The user refreshes the page to try again (to create Order). We should get the new IdempotencyKey if the order has been created, and the same IdempotencyKey if it hasn't, right?

    Approach A: This accepted answer here suggests that the client sets a unique ID, to whatever it likes, and the server knows to only ever allow one instance thereof (to enforce uniqueness, that doesn't mean it has to generate it). I'm unclear if the ID that answer refers to is the ObjectID or a IdempotencyKey. In your scenario you would want to keep the same IdempotencyKey otherwise it will look like a new request.

    Other answers from that same question discuss POE, which seems to be along the lines you were originally suggesting.

    Approach B: Ensure the server-side service has sufficient smarts to see if the incoming data was already in existence, this could be based just on the incoming objects data, or could also take information like the requests date/time and the objects created date/time into account.

    Update - Extra Thoughts

    The difference between approaches A & B is that with B you are essentially checking against the actual data (presumably in the System of Record i.e. the master), with A you're only validating client requests, which are partially outside your control (you can tell API clients what to do but you can't control how they do it, beyond what your API can enforce).

    With B the onus is on the service to ensure the consistency of the data, meaning it (by design) will/should assume API callers have no idea what they are doing. With A you'll only be able to do this if the idempotency is enforced by the server - meaning both the client and server need to work together. E.g. what happens if some naughty client-request is missing the IdempotencyKey? Will you/the server reject the request? What happens if some lazy client-app coder just hard-codes the same value - or if two clients accidentally submit clashing IdempotencyKey?