Understanding and Implementing Idempotency in Backend Services with Java and Redis

This article explains the concept of idempotency, identifies which API requests are naturally idempotent, discusses why idempotency is essential for retries, asynchronous callbacks, and message queues, and provides a step‑by‑step Java Spring implementation using custom annotations, AOP, and Redis for token management.

Java Captain
Java Captain
Java Captain
Understanding and Implementing Idempotency in Backend Services with Java and Redis

Idempotency means that multiple identical requests to an interface produce the same result as a single request, which requires careful system design to guarantee.

Read‑only queries are naturally idempotent, and delete operations are usually idempotent except in ABA scenarios; updates are generally idempotent unless they involve non‑deterministic expressions such as update table set a = a + 1 where v = 1. Inserts are typically non‑idempotent unless a unique database index prevents duplicates.

Idempotency is needed for timeout retries (so a second request does not cause duplicate effects), asynchronous callbacks (which must be safe to repeat), and message‑queue consumption (where at‑least‑once delivery can cause duplicate messages).

The key factors for implementing idempotency are a unique identifier (token) generated per request and ensuring the server uses this identifier only once, often by leveraging a database unique index.

Implementation using annotations:

1. Define a 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 {
    String name() default "";
    String field() default "";
    Class type();
}

2. Create a unified request data object:

@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. Implement an AOP aspect to enforce idempotency:

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 "重复请求";
    }
}

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().toUpperCase().equals(name.toUpperCase())) {
                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;
    }
}

4. Generate token values and store them in Redis:

import com.springboot.micrometer.idempotent.RedisIdempotentStorage;
import com.springboot.micrometer.util.IdGeneratorUtil;
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;
    }
}

public interface IdempotentStorage {
    void save(String idempotentId);
    boolean delete(String idempotentId);
}

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;

@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);
    }
}

import java.util.UUID;

public class IdGeneratorUtil {
    public static String generateId() {
        return UUID.randomUUID().toString();
    }
}

5. Example controller using the @Idempotent annotation:

@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";
    }
}

Clients first request a token, then include the token in the subsequent order request; the first request succeeds, while a repeated request with the same token is rejected, demonstrating effective idempotency enforcement.

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.

BackendJavaaopredisspringIdempotency
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.