Backend Development 20 min read

Implementing API Idempotency in SpringBoot Using Token and Redis

This article explains the concept of idempotency, why it is needed for HTTP interfaces, its impact on system design, and presents four practical backend solutions—including database primary keys, optimistic locking, anti‑repeat tokens, and downstream sequence numbers—followed by a complete SpringBoot example with Maven dependencies, Redis configuration, token utility code, controller, application starter, and test cases.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing API Idempotency in SpringBoot Using Token and Redis

Idempotency is a mathematical and computing concept where executing an operation multiple times yields the same result as executing it once; in web services it ensures that repeated HTTP requests do not cause additional side effects.

The article describes common scenarios that require idempotency, such as duplicate form submissions, malicious repeated voting, client‑side timeout retries, and message re‑consumption, and explains that while idempotency simplifies client logic, it adds complexity to server implementation.

It reviews the idempotency characteristics of RESTful HTTP methods, indicating which methods are inherently idempotent (GET, PUT, DELETE, HEAD, OPTIONS, TRACE) and which are not (POST).

Four backend implementation strategies are detailed:

Database unique primary key : Use a globally unique ID as the primary key to guarantee single insertion or deletion.

Database optimistic lock : Add a version field to rows and update only when the version matches, preventing duplicate updates.

Anti‑repeat token : Generate a UUID token, store it in Redis with a short TTL, and verify/delete it atomically via a Lua script.

Downstream unique sequence number : The downstream service supplies a unique sequence number that the upstream service combines with a credential ID to form a Redis key for idempotency checks.

The article then provides a step‑by‑step SpringBoot implementation of the token‑based solution.

1. Maven dependencies

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
  </parent>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
    <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
  </dependencies>
</project>

2. Redis configuration (application.yml)

spring:
  redis:
    ssl: false
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1000
    lettuce:
      pool:
        max-active: 100
        max-wait: -1
        min-idle: 0
        max-idle: 20

3. Token utility service

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class TokenUtilService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";

    public String generateToken(String value) {
        String token = UUID.randomUUID().toString();
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        return token;
    }

    public boolean validToken(String token, String value) {
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript
redisScript = new DefaultRedisScript<>(script, Long.class);
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        if (result != null && result != 0L) {
            log.info("Token validation succeeded: token={}, key={}, value={}", token, key, value);
            return true;
        }
        log.info("Token validation failed: token={}, key={}, value={}", token, key, value);
        return false;
    }
}

4. Test controller

import lombok.extern.slf4j.Slf4j;
import mydlq.club.example.service.TokenUtilService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class TokenController {
    @Autowired
    private TokenUtilService tokenService;

    @GetMapping("/token")
    public String getToken() {
        String userInfo = "mydlq"; // simulated user info
        return tokenService.generateToken(userInfo);
    }

    @PostMapping("/test")
    public String test(@RequestHeader(value = "token") String token) {
        String userInfo = "mydlq";
        boolean result = tokenService.validToken(token, userInfo);
        return result ? "正常调用" : "重复调用";
    }
}

5. SpringBoot entry point

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

6. Integration test

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class IdempotenceTest {
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Test
    public void interfaceIdempotenceTest() throws Exception {
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        String token = mockMvc.perform(MockMvcRequestBuilders.get("/token").accept(MediaType.TEXT_HTML))
                .andReturn().getResponse().getContentAsString();
        log.info("Fetched token: {}", token);
        for (int i = 1; i <= 5; i++) {
            String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
                    .header("token", token)
                    .accept(MediaType.TEXT_HTML))
                    .andReturn().getResponse().getContentAsString();
            log.info("Call {} result: {}", i, result);
            if (i == 1) {
                Assert.assertEquals("正常调用", result);
            } else {
                Assert.assertEquals("重复调用", result);
            }
        }
    }
}

The final section summarizes that idempotency is essential for payment‑related services and recommends choosing the appropriate implementation method based on the business scenario: unique primary key for insert‑only operations, optimistic lock for updates, downstream sequence numbers for upstream‑downstream coordination, and token‑Redis for generic duplicate‑submission protection.

javaRedisIdempotencySpringBoottokenRESTful API
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login 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.