AppUnite LogoBlog
< Go to homepage
Contact
Engineering

Make your system more predictable by using idempotent interfaces

| 2 min read

Let's analyze how a toggle button that turns activity on and off can be designed and implemented, on the server-side.

Lack of idempotency leads to unexpected results

Since it is called a toggle in UI, let's call the interface on the server-side that way. A user clicks the toggle, and the app sends the request:

PATCH /api/user/activity_status/toggle

and the system runs the following interface:

def toggle(user_id) do
  user_status = get(user_id)

  if user_status.active do
    :ok = user_turns_off_active_status(user_id)
  else
    :ok = user_turns_on_active_status(user_id)
  end
  
  :ok
end

Does it work? Yes, it does. But what happens when the request is sent twice by accident? How do we want our system to react? Should the user be active or inactive?

First of all, the interface is not idempotent. It means that invoking the function several times doesn't produce the same result. Finally, the name of the interface is deprived of ubiquitous language [E. Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, p. 24] that could indicate what result is expected.

There is one more problem. What if the system automatically turns off the active status after 10 PM? We need to add the if statement to verify whether the user is active or not to make them inactive:

if UserActivity.get(user_id).active do
  :ok = UserActivity.toggle(user_id)
end

Has it helped? Let's see what happens, if at the same time, the user tries to turn off their active status and the system does it too?

When both requests reach the if statement the user is active. Then, the user's request invokes the toggle/1 function and turns off active status. Right after, the worker runs the same interface. This time the status changes back to active as the user's request has just turned the active status off.

To guarantee the user gets the expected result, we need to do concurrency control by wrapping the logic into the transaction and locking the table. As a result, simple behavior that has been implemented incorrectly forced us to add extra lines of code.

Using idempotent interfaces to change state

The one way to avoid mentioned problems is to modify endpoint and pass parameters that clearly indicate what results are expected:

PATCH /api/user/activity_status

{
  active: true/false
}

Then the controller decides which interface should be run based on the value of the active field:

def activity_status(conn, %{active: active}) do
  user_id = conn.assigns.user_id

  if active do
    UserActivity.user_turns_on_active_status(user_id) 
  else
    UserActivity.user_turns_off_active_status(user_id)
  end

  # ...
end

Names of interfaces say about the end-state, so invoking one of them, you know exactly what happens. Even if you do that several times, the result will always be the same. What's more, they clearly show how the domain works, so you don't need to wonder what the purpose of these interfaces is.

This website stores cookies on your computer. The data is used to collect information about how you interact with our website and allow us to remember you. We use this information to improve and customize your browsing experience and for analytics and metrics about our visitors both on this website and other media. Cookie Policy Privacy Notice

We'd love to build something amazing together!

Make the first step for a great partnership! Share your idea with us and check what we can do for you and your company.

Start a project
ClutchFinancial TimesForbes