Make your endpoints idempotent easily
- First of all, you need to add a dependency to pom.xml
For Redis:
<dependency>
<groupId>com.trendyol</groupId>
<artifactId>Jdempotent-spring-boot-redis-starter</artifactId>
<version>1.1.0</version>
</dependency>
For Couchbase:
<dependency>
<groupId>com.trendyol</groupId>
<artifactId>Jdempotent-spring-boot-couchbase-starter</artifactId>
<version>1.1.0</version>
</dependency>
- You should add
@IdempotentResource
annotation to the method that you want to make idempotent resource, listener etc.
@IdempotentResource(cachePrefix = "WelcomingListener")
@KafkaListener(topics = "trendyol.mail.welcome", groupId = "group_id")
public void consumeMessage(@IdempotentRequestPayload String emailAdress) {
SendEmailRequest request = SendEmailRequest.builder()
.email(message)
.subject(subject)
.build();
try {
mailSenderService.sendMail(request);
} catch (MessagingException e) {
logger.error("MailSenderService.sendEmail() throw exception {} event: {} ", e, emailAdress);
// Throwing any exception is enough to delete from redis. When successful, it will not be deleted from redis and will be idempotent.
throw new RetryIdempotentRequestException(e);
}
}
If want that idempotencyId in your payload. Put @JdempotentId
annotation that places the generated idempotency identifier into annotated field.
Can be thought of as @Id annotation in jpa.
public class IdempotentPayload {
@JdempotentId
private String jdempotentId;
private Object data;
}
You might want to handle the name of the field differently to ensure idempotency. Just use @JdempotentProperty annotation needs to get the field name differently and generate the hash inspired by jackson (@JsonProperty annotation)
public class IdempotentPayload {
@JdempotentProperty("userId")
private String customerId;
private Object data;
}
- If you want to handle a custom error case, you need to implement
ErrorConditionalCallback
like the following example:
@Component
public class AspectConditionalCallback implements ErrorConditionalCallback {
@Override
public boolean onErrorCondition(Object response) {
return response == IdempotentStateEnum.ERROR;
}
public RuntimeException onErrorCustomException() {
return new RuntimeException("Status cannot be error");
}
}
- Let's make the configuration:
For redis configuration:
jdempotent:
enable: true
cache:
redis:
database: 1
password: "password"
sentinelHostList: 192.168.0.1,192.168.0.2,192.168.0.3
sentinelPort: "26379"
sentinelMasterName: "admin"
expirationTimeHour: 2
dialTimeoutSecond: 3
readTimeoutSecond: 3
writeTimeoutSecond: 3
maxRetryCount: 3
expireTimeoutHour: 3
For couchbase configuration:
jdempotent:
enable: true
cryptography:
algorithm: MD5
cache:
couchbase:
connection-string: XXXXXXXX
password: XXXXXXXX
username: XXXXXXXX
bucket-name: XXXXXXXX
connect-timeout: 100000
query-timeout: 20000
kv-timeout: 3000
Please note that you can disable Jdempotent easily if you need to. For example, assume that you don't have a circuit breaker and your Redis is down. In that case, you can disable Jdempotent with the following configuration:
enable: false
@SpringBootApplication(
exclude = { RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class }
)
As it is shown in the following image, the most cpu consuming part of Jdempotent is getting a Redis connection so we don't need to worry performance related issues.
Jdempotent Medium Article
Jdempotent-core Javadoc
Jdempotent-spring-boot-redis-starter Javadoc
- Fork it ( https://github.com/Trendyol/Jdempotent/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
- memojja Mehmet ARI - creator, maintainer