All happy servers are alike; each unhappy server is part of a distributed system.

Most modern software systems are distributed systems in some way. You may not be deploying a multi-cloud microservices app, but you are probably using a database, a message queue, or a cache. You almost certainly need to call out to third-party APIs in order to get work done like process payments or send notifications.

If your application depends on other systems that are distributed over a network, then you are building a distributed system, and you need to think through some of the fallacies of distributed computing.

One of the most common failures in a distributed system is a network partition. This is when your application can't reach other systems over the network. Perhaps the network is suddenly too slow and your requests time out, or perhaps your own application crashed while handling a request.

Let's say your application needs to trigger a Knock workflow. You send a request to the Knock API to start a workflow. The request is successful, but the response is somehow lost in transit. Your application doesn't know if the request was successful. If you retry the request, you may end up with two workflows running in parallel. This is a problem.

Example of two network requests without idempotency

That's where idempotency comes in. Apart from being easy to misspell and hard to pronounce, idempotency makes distributed systems more reliable. In this post, we'll look at idempotent requests: What they are, why they are important, and how to implement them in your application.

Better yet, if you use Elixir, you can use our new One and Done library to make it trivial to implement idempotent requests in your application. We developed this package to support idempotency at Knock, and we're excited to share it with the community.

What is an idempotent request?

An idempotent request is a request that can be safely repeated without unintended side effects. The request may be retried, or it may be sent multiple times in parallel. The result of the request will be the same regardless of how many times it is sent.

In our previous example, if you send a request to start a Knock workflow and you pass an Idempotency-Key header, then the Knock API will do a few things:

  1. Check if we have already handled a request with the same Idempotency-Key value.
  2. If we have, then return the same response as before.
  3. If we haven't, then process the request, return the response, and cache that response for future requests with the same Idempotency-Key.

Example of two network requests using an idempotency key

Knock will remember the response for 24 hours. If you send a request with the same Idempotency-Key after 24 hours, then Knock will process the request again and return a new response.

Subsequent requests are very fast, so you can retry requests as often as you need to without worrying about performance.

This means that, if your application experiences some of the pains of being a distributed system, you can safely retry the request without worrying about unintended side effects.

That's why we call our new library "One and Done".

How to use One and Done

One and Done is an Elixir library that makes it easy to implement idempotent requests in your application. It supports the Plug web framework, so you can use it with Phoenix, Plug, or any other web framework that supports Plug.

Its only dependency is a cache to store the responses. You can use any cache that supports the Cache behaviour. Although designed for Nebulex, you can adapt One and Done to work with any cache.

Here's an example of how to use One and Done in a Phoenix router:

# lib/my_app_web/router.ex

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :api do
    # Other plugs go here
    plug OneAndDone.Plug, cache: MyAppWeb.Cache
  end

  scope "/api", MyAppWeb do
    pipe_through :api

    post "/workflows", WorkflowController, :start
  end
end

That's it! Now, if you send a POST request to /api/workflows with an Idempotency-Key header, then One and Done will check the cache to see if we have already handled a request with the same Idempotency-Key value for this method and path. If we have, then One and Done will return the cached response. If we haven't, then One and Done will process the request, cache the response, and return the response. Any other POST or PUT calls under the /api scope in this router will also support idempotency. Requests that don't have an Idempotency-Key header will be processed normally, making it possible to opt-in to idempotent behavior.

Check out the One and Done documentation for more details on how it caches responses, how to configure it for more advanced use cases, and how to use it with other web frameworks. It's permissively licensed under the MIT license so you can use it in your open source or commercial projects. One and Done is designed to be extremely flexible since how idempotency works can vary from application to application. If you have any feedback or suggestions, please open an issue or send us a pull request.

Knock SDK support

Knock offers SDKs in multiple languages. Each SDK supports idempotency for workflow triggers, with support for other API endpoints coming soon. If you use the Knock SDK in your application, then you can start using idempotency today. Check out the Knock documentation for more details.