An Idempotency Library: Jdempotent
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.
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.welcomemehmet.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.
Let’s change the smtp credentials to get an auth exception so that we expect the payload to be saved to Redis first, then it will be deleted after throwing an exception. Since the operation is not completed successfully, next request with the same payload will be handled successfully.
Result:
Let’s repeat the same scenario for the http post endpoint.
Results:
In a nutshell
This library helps us make our app idempotent on the fly. You can also use it for api caching.
You can access the previous examples from this link and you can access the library from this link.
We are waiting for your contributions :)
References:
Thanks for reading,