Implementing Idempotency in Spring Boot with Custom Annotations and Redis

This article explains the concept of idempotency, identifies naturally idempotent HTTP methods, discusses why idempotency is essential for retries, async callbacks, and message queues, and provides a complete Spring Boot implementation using a custom @Idempotent annotation, AOP, and Redis for token storage.

Top Architect
Top Architect
Top Architect
Implementing Idempotency in Spring Boot with Custom Annotations and Redis

1. What Is Idempotency?

In simple terms, idempotency means that executing the same API request multiple times yields the same result as a single execution. Achieving true idempotency in production systems requires careful design, as every interface should behave idempotently.

2. Which Requests Are Naturally Idempotent?

Query (GET) requests are inherently idempotent. Delete (DELETE) requests are also idempotent in most cases, except for ABA scenarios where a deleted resource might be recreated between calls.

Example

If a delete request for entity A times out and is retried, but another A is inserted between the two calls, the second delete would incorrectly remove the newly inserted record – this is the ABA problem, which is usually negligible.

Update (PUT/PATCH) operations are generally idempotent, except when the update statement itself is non‑idempotent, such as update table set a = a + 1 where v = 1. Insert (POST) operations are typically non‑idempotent unless a unique database index guarantees no duplicate data.

3. Why Do We Need Idempotency?

1. Timeout Retry

When an RPC request fails due to network instability, the client may retry. The server must ensure that the repeated request does not cause side effects.

2. Asynchronous Callbacks

Asynchronous callbacks are used to improve throughput; such endpoints must be idempotent to handle possible duplicate invocations.

3. Message Queues

Message queue frameworks like Kafka, RocketMQ, and RabbitMQ follow the “at‑least‑once” delivery guarantee, which can produce duplicate messages. Consumers therefore need to implement idempotent processing logic.

4. Key Factors for Implementing Idempotency

Factor 1: Idempotent Token

An idempotent token (or global ID) uniquely identifies a client request. It is usually generated by the client, but can also be provided by a third‑party service.

Factor 2: One‑Time Use

The server must ensure that each token is used only once, commonly by leveraging a unique constraint in the database.

5. Implementing Idempotency with Annotations

Below is a step‑by‑step demonstration using Redis for token storage.

1. Custom Annotation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /** Parameter name from which the token will be extracted */
    String name() default "";
    /** Field name inside the parameter that holds the token */
    String field() default "";
    /** Parameter type */
    Class type();
}

2. Unified Request Wrapper

@Data
public class RequestData<T> {
    private Header header;
    private T body;
}

@Data
public class Header {
    private String token;
}

@Data
public class Order {
    String orderNo;
}

3. AOP Aspect

import com.springboot.micrometer.annotation.Idempotent;
import com.springboot.micrometer.entity.RequestData;
import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;

@Aspect
@Component
public class IdempotentAspect {
    @Resource
    private RedisIdempotentStorage redisIdempotentStorage;

    @Pointcut("@annotation(com.springboot.micrometer.annotation.Idempotent)")
    public void idempotent() {}

    @Around("idempotent()")
    public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Idempotent idempotent = method.getAnnotation(Idempotent.class);

        String field = idempotent.field();
        String name = idempotent.name();
        Class clazzType = idempotent.type();

        String token = "";
        Object object = clazzType.newInstance();
        Map<String, Object> paramValue = AopUtils.getParamValue(joinPoint);
        if (object instanceof RequestData) {
            RequestData idempotentEntity = (RequestData) paramValue.get(name);
            token = String.valueOf(AopUtils.getFieldValue(idempotentEntity.getHeader(), field));
        }

        if (redisIdempotentStorage.delete(token)) {
            return joinPoint.proceed();
        }
        return "Duplicate request";
    }
}

4. Utility Classes

public class AopUtils {
    public static Object getFieldValue(Object obj, String name) throws Exception {
        Field[] fields = obj.getClass().getDeclaredFields();
        Object object = null;
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.getName().equalsIgnoreCase(name)) {
                object = field.get(obj);
                break;
            }
        }
        return object;
    }

    public static Map<String, Object> getParamValue(ProceedingJoinPoint joinPoint) {
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> param = new HashMap<>(paramNames.length);
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }
}

5. Token Generation

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@RequestMapping("/idGenerator")
public class IdGeneratorController {
    @Resource
    private RedisIdempotentStorage redisIdempotentStorage;

    @RequestMapping("/getIdGeneratorToken")
    public String getIdGeneratorToken() {
        String generateId = IdGeneratorUtil.generateId();
        redisIdempotentStorage.save(generateId);
        return generateId;
    }
}

6. Storage Interface and Redis Implementation

public interface IdempotentStorage {
    void save(String idempotentId);
    boolean delete(String idempotentId);
}
@Component
public class RedisIdempotentStorage implements IdempotentStorage {
    @Resource
    private RedisTemplate<String, Serializable> redisTemplate;

    @Override
    public void save(String idempotentId) {
        redisTemplate.opsForValue().set(idempotentId, idempotentId, 10, TimeUnit.MINUTES);
    }

    @Override
    public boolean delete(String idempotentId) {
        return redisTemplate.delete(idempotentId);
    }
}

7. Order Controller Example

@RestController
@RequestMapping("/order")
public class OrderController {
    @RequestMapping("/saveOrder")
    @Idempotent(name = "requestData", type = RequestData.class, field = "token")
    public String saveOrder(@RequestBody RequestData<Order> requestData) {
        return "success";
    }
}

6. Request Flow Example

First, call the token endpoint to obtain a unique token. Then include this token in the request header when invoking the order‑saving API. The first call succeeds; a subsequent call with the same token is rejected as a duplicate.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaredisSpring BootIdempotencyannotation
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.