Idempotency in API Design

Idempotency refers to the property of an operation where the result remains the same regardless of how many times it is applied. In other words, performing an idempotent operation multiple times has the same effect as performing it just once. This characteristic is crucial for ensuring the consistency and reliability of systems, particularly in distributed environments where network failures and retries are common.

Idempotency in API Design

In the context of APIs, idempotency plays a pivotal role in guaranteeing the integrity of operations, especially those that involve creating, updating, or deleting resources. When an API endpoint is idempotent, clients can safely retry requests without fear of unintended side effects or duplicate actions.

Idempotency on Object Creation

The first approach is associated with the creation of an object. When a client sends a request to create a new resource, it includes a unique idempotency key along with the payload. The server then uses this key to deduplicate requests, ensuring that the resource is created only once, even if the client retries the request multiple times due to network issues or other failures.

In this approach the backend of the API stores the idempotency key as part of the object. The field is usually called an idempotency_key or a reference:

Subscription:
{
  "id": "a5d067f9-fa7f-4bbd-ace4-a5dfc4b97e1e",
  "price": "20.55",
  "state": "CREATED",
  "idempotency_key": "c089f067-6a47-4f1d-98ab-47c529b246ef", // provided by the client
  "created_at": "2023-03-23T14:58:00Z",
  "updated_at": "2023-03-23T14:58:00Z"
}

Idempotency on Actions

Alternatively, idempotency keys can be tied to specific actions rather than individual objects. In this approach, each object maintains a relation to one or more idempotency keys, which are used to identify and deduplicate requests for different operations (e.g., updates or deletions) on that object. This allows for finer-grained control over idempotency while still ensuring consistency across related actions.

One example of using this approach is Stripe API. Example:

Subscription:
{
  "id": "a5d067f9-fa7f-4bbd-ace4-a5dfc4b97e1e",
  "price": "20.55",
  "state": "CREATED",
  "created_at": "2023-03-23T14:58:00Z",
  "updated_at": "2023-03-23T14:58:00Z"
}
Idempotency object:
{
  "id": "74150c36-80ae-4935-8549-d8d8cd256bad",
  "resource": "Subscription",
  "resource_id": "a5d067f9-fa7f-4bbd-ace4-a5dfc4b97e1e",
  "action": "create",
  "idempotency_key": "c089f067-6a47-4f1d-98ab-47c529b246ef", // provided by the client
  "created_at": "2023-03-23T14:58:00Z",
  "updated_at": "2023-03-23T14:58:00Z"
}

What Approach to Choose?

In many cases, updates can be considered inherently idempotent. For instance, if you're updating a resource attribute like a user's name from "Tom" to "John", performing the update operation multiple times will result in the same final state, "John". This characteristic aligns with the definition of idempotency, where the outcome remains consistent regardless of how many times the operation is applied.

However, it's important to note that not all update operations may exhibit this property, especially in more complex systems where the update process involves multiple steps or external dependencies. In such scenarios, ensuring idempotency becomes more challenging, and explicit handling may be necessary.

Let's consider an example where an update operation involves not just modifying an attribute but also triggering additional actions or workflows. In such cases, the idempotency of the overall operation might be influenced by factors beyond the direct modification of the resource. For instance, if updating a user's profile triggers an email notification to be sent, ensuring idempotency requires handling potential duplication of emails in case of retries.

Further read

Implementing idempotency keys makes API endpoints deterministic. Read on how to make actions atomic.