Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Idempotency in Distributed Systems: Payment Processing Risks

Hello! Welcome back to our course on distributed systems architecture.

Introduction

In our previous lesson, we analyzed the trade-offs between synchronous and asynchronous communication. We concluded that while asynchronous patterns provide excellent resilience and scalability, they introduce a new challenge: if a consumer service fails after processing a message but before acknowledging it, the message broker might redeliver it. For many operations, especially in finance, this could lead to disastrous duplicate actions, like charging a customer twice.

Today, we will confront this problem head-on. This lesson introduces idempotency, a fundamental concept for building reliable distributed systems. We'll explore why it's so critical and examine the standard architectural pattern used to implement it.

Learning Outcome:

Explain the role of idempotency in distributed systems, using examples from payment processing to illustrate the risks of non-idempotent operations.

Your experience at Revolut, particularly with clearing, settlement, and FX trading systems, will provide a strong foundation for understanding the high stakes involved and the necessity of the patterns we'll discuss.


1. The Core Problem: The Ambiguity of Failure

In a distributed system, when a client sends a request to a server, there are three general classes of failure, two of which create ambiguity.

Imagine your TradingService sends a request to a PaymentGateway to execute a payment.

  1. Pre-Server Failure: The request never reaches the PaymentGateway due to a network issue. The client gets a connection error. In this case, it is safe to retry.
  2. Mid-Processing Failure: The PaymentGateway receives the request and starts processing (e.g., debiting an account), but it crashes before completing the operation and responding. The client gets a timeout.
  3. Post-Processing Failure: The PaymentGateway successfully processes the payment, but the network fails before the success response can reach the TradingService. The client gets a timeout.

In scenarios 2 and 3, the client is left in an uncertain state. It doesn't know if the payment was processed. The natural reaction is to retry, but as you can imagine, retrying a payment request could result in a double charge.

This video provides a clear illustration of this exact problem.

Build a robust Payments service using Idempotency Keys

The video 'Build a robust Payments service using Idempotency Keys' by Arpit Bhayani clearly explains why transactions might be repeated in a distributed system.

Please watch the section 'Why Transactions Repeat: The Problem Statement' from 03:55 to 07:12. Pay close attention to the diagram showing the payment service and payment gateway, and how a lost response leads to a dangerous retry.

This ambiguity is the root cause of many data consistency issues in distributed systems. To solve it, we need to make our operations idempotent.


2. What is Idempotency?

In mathematics and computer science, an operation is idempotent if applying it multiple times produces the same result as applying it once.

  • Naturally Idempotent Operation: Setting an object's state to a specific value. For example, UPDATE users SET email = 'test@example.com' WHERE user_id = 123;. Running this command ten times has the same final effect as running it once.
  • Non-Idempotent Operation: An operation that modifies state based on the current state. For example, UPDATE accounts SET balance = balance - 10 WHERE account_id = 456;. Running this command twice will deduct 20, which is different from running it once.

Payment processing, order placement, and message sending are all classic examples of non-idempotent operations. The challenge is to design our system in a way that allows us to safely retry these non-idempotent operations. The goal is to achieve exactly-once processing semantics on top of a system that may offer at-least-once delivery of messages or requests.


3. The Solution: The Idempotency Key Pattern

The standard industry solution, famously used by payment processors like Stripe, is the Idempotency Key pattern. The client generates a unique key for each distinct operation and includes it in the request. The server then uses this key to recognize and de-duplicate retried requests.

The following article provides an excellent overview of this pattern, using Stripe's API as a model.

Designing Idempotent API Endpoints for Payments

The article 'Designing Idempotent API Endpoints for Payments' explains the architecture and workflow of the idempotency key pattern in detail.

Please read the sections from 'Stripe’s Approach to Idempotent API Design' through the end of the 'Idempotent Response' section. Focus on understanding the role of the Idempotency-Key header, the three architectural components (API Server, Payment DB, Key-Value Store), and the step-by-step flow of an initial request versus a retry.

The Workflow in Detail

Let's consolidate the key steps of the flow described in the article:

  1. Client-Side Key Generation: Before sending a request for a state-changing operation (like a payment), the client generates a unique identifier (e.g., a UUID). This is the idempotency key.
  2. Request with Key: The client sends the API request, including the idempotency key, typically in an HTTP header like Idempotency-Key: <unique-key>.
  3. Server-Side Check: When the server receives the request, the first thing it does is check for the presence of the idempotency key in a persistent, fast-access store (like Redis or a dedicated database table).
  4. Handling the Request:
    • If the key is new: The server "locks" the key to prevent concurrent processing of the same request. It then proceeds with the business logic (e.g., charging the card). Upon completion, it saves the result (the HTTP status code and response body) associated with the key and releases the lock. Finally, it sends the response to the client.
    • If the key is found and the original operation is complete: The server immediately stops processing. It retrieves the saved response associated with that key and sends it back to the client. The business logic is not executed again.

This ensures that even if the client retries due to a timeout (the "Post-Processing Failure" scenario), the payment is only processed once. The client receives the original success response on its second attempt and can confidently move on.

The following video provides a dynamic walkthrough of this "check and update" logic.

Build a robust Payments service using Idempotency Keys

Let's return to Arpit Bhayani's video to see a visual explanation of the implementation logic.

Watch the section 'Implementing Idempotency with a Unique Payment ID' from 09:41 to 16:10. The video uses the term 'Payment ID', which serves the same function as an 'Idempotency Key'. Notice how this single ID is used to tie the entire transaction together and how the server checks the status of the ID before processing a retry.


4. Nuances and Broader Applications

Is Idempotency Always Necessary?

Implementing this pattern adds complexity. For non-critical operations, a simpler approach might be sufficient. As the video Build a robust Payments service using Idempotency Keys mentions (in the section "When Idempotency Might Not Be Necessary"), you could choose not to build automatic retry logic and instead propagate an error to the user, asking them to initiate the action again explicitly. This is a trade-off between system resilience and implementation complexity. For a payment system, the complexity of idempotency is almost always justified; for a "like button" on a social media post, perhaps not.

Applications Beyond Payments

The idempotency key pattern is a general-purpose tool for reliability and is not limited to payments. It is valuable for any critical, non-idempotent operation in a distributed system:

  • User Registration: Prevents creating duplicate user accounts if a sign-up form is submitted twice.
  • Order Placement: Ensures a customer's order is not duplicated if they double-click the "Place Order" button.
  • Triggering Workflows: Guarantees that a complex and expensive business process (e.g., provisioning a new service) is only initiated once.

Conclusion

Today you've learned how to solve one of the most common and dangerous problems in distributed systems: the risk of duplicate operations caused by retries. By understanding and applying the idempotency key pattern, you can build systems that are robust against network failures and provide "exactly-once" processing guarantees.

Key Takeaways:

  • Network failures in distributed systems create ambiguity, making it unsafe to blindly retry non-idempotent operations like payments.
  • Idempotency is the property where an operation can be repeated multiple times with the same effect as performing it once.
  • The Idempotency Key pattern is the industry-standard solution. The client generates a unique key, and the server uses it to de-duplicate requests, ensuring business logic is executed only once for a given operation.
  • This pattern allows you to build systems with exactly-once processing semantics on top of infrastructure that may only guarantee at-least-once delivery.

Preview of the Next Lesson:

Now that we know how to make retries safe, the next question is how to perform them smartly. Simply retrying immediately might not be effective if the remote service is temporarily overloaded or down. In our next lesson, we will learn how to implement client-side retry logic with exponential backoff and jitter, a crucial technique for handling transient failures gracefully without overwhelming a struggling service.

Can't find a good explanation? Sign up and we'll make it for you

Sign up