An Idempotency Library: Jdempotent
By: Mehmet Ari
Tags: Kafka, Idempotency, Idempotent Consumer, Rabbitmq, Idempotency Library

In this post, we are going to talk about an open source project we built as customer services team at Trendyol. First of all I want to explain what we are doing as the Customer Services team.

Problem & Solution

As the Customer Services team, we are responsible for the development of processes such as customer omni channels (email, push and sms), ivr (instant voice reply) system, screens used by customer services. Therefore, as you can imagine, we process millions of events every day. They include many events such as receiving an order, delivering the order, cancelling the order due to fraud, and defining a discount coupon for the customer and many more. Multiple processing of the same event can cause serious customer dissatisfaction. This situation may have more than one reason. For instance, X team may keep events in the outbox table for a week and can republish these events for any reason. As a transactional consumer, we have to handle this situation so that we don't process the same event again. That's why it was very important for us to solve this problem. While solving this problem, we thought about how we could make it more generic and then developed our tool library named as Jdempotent . Actually, Jdempotent is a library to provide idempotency, as the name suggests :)

Let’s take a look at what idempotency means by a quote from Wikipedia,

Making multiple identical requests has the same effect as making a single request. Note that while idempotent operations produce the same result on the server (no side effects), the response itself may not be the same (e.g. a resource’s state may change between requests).

We can say that Jdempotent is the implementation of this definition using Java. You can make any Kafka consumer, Rabbitmq consumer, grpc endpoint, http endpoint etc. idempotent with Jdempotent. In short, you can make any resource idempotent on the fly. Just add the @IdempotentResource annotation above the method that should be idempotent. For now, we have in-memory and redis support as datasource preference also if you want to use a different datasource, you can do it by extending to Jdempotent-core project. ( Jdempotent-spring-boot-redis-starter uses Jdempotent-core .)

Our goal is to support various datasources like Hazelcast and Couchbase also you are more than welcome to contribute :)

Let’s see how we can implement the structure easily via the sample email sender application. Following email sender application has two parts. These include a kafka listener and an http post endpoint that show multiple use cases of Jdempotent. Try to send the first http request or Kafka event to the email sender application. If this process succeeds, the event will be saved to Redis by Jdempotent to be idempotent. Subsequent requests will not be processed at all, and the corresponding response will be read from Redis and return. Thus, we do not execute the same request again.

Mail sender application architecture

Let’s code in 4 steps.

First, let’s add your Redis configuration where we will keep the hash of the incoming requests.

2. Since this example will be a simple mail sending application, let’s code the mail sender’s service layer implementation.

3. Now, let’s write a Kafka listener to show that @IdempotentResource can be used. Notice @IdempotentResource(cachePrefix = "WelcomingListener") this annotation will make the incoming emailAddress object and idempotent element. Also, the optional cachePrefix value, will make the hash key more collider.

4. There are different ways of Jdempotent . For example, we implement a new POST Method in MailController via a using @IdempotentResource annotation.

Explain

As you can see above, we only put an @IdempotentResource annotation, we didn't spend any extra efforts. Let's try to understand what happened internally. There is an around aspect that listens for methods tagged with @IdempotentResource annotation.

It generates a hash value from the object, if the method has one argument, get the first otherwise the object tagged with @IdempotentPayload is to save to Redis with the specified TTL(time to live). In case of any exception, the payload will be deleted from Redis because the process has not been completed.

In a nutshell:

Not competed successfully: we should be able to execute this request again because it hasn’t been processed yet.

Competed successfully: if no exception is thrown, it returns the response written from Redis.

The process is very simple :)

Let’s send 3 event to the kafka topic at same time

kafka-console-producer --broker-list localhost:9092 --topic trendyol.mail.welcome mehmet.ari@gmail.com

mehmet.ari@gmail.com

mehmet.ari@gmail.com

Result:

As it is shown in the following logs, the first request is saved to Redis. For other subsequent requests, the execution will be halted since an equivalent payload exists in Redis.