Understanding Idempotency and Its Implementation in RESTful APIs

This article explains the concept of idempotency, why it is essential for HTTP interfaces, the impact on system design, and presents several practical implementation strategies—including unique primary keys, optimistic locking, anti‑repeat tokens, and downstream sequence numbers—accompanied by a complete Spring Boot example with Redis integration and test code.

Top Architect
Top Architect
Top Architect
Understanding Idempotency and Its Implementation in RESTful APIs

Idempotency is a mathematical and computing concept where applying an operation multiple times yields the same result as applying it once; in programming, an idempotent operation produces identical effects regardless of how many times it is executed.

HTTP/1.1 defines idempotent methods, meaning that repeated requests to a resource should have the same effect as a single request (excluding network errors). This property prevents unintended side effects such as duplicate form submissions, malicious vote spamming, client‑side retries, or message re‑consumption.

Implementing idempotency simplifies client logic but adds complexity on the server side, potentially reducing parallelism and requiring extra business logic.

RESTful HTTP methods and their idempotency status:

GET, HEAD, PUT, DELETE, OPTIONS, TRACE – idempotent (✓)

POST – not idempotent (✗)

PATCH – may be idempotent depending on business logic (–)

Four common server‑side solutions are described:

1. Database Unique Primary Key

Use a globally unique primary key (often a distributed ID) to ensure that insert or delete operations cannot be duplicated. The database’s unique constraint guarantees idempotency.

2. Optimistic Lock

Add a version column to a table; each update includes the expected version value. The SQL

UPDATE my_table SET price=price+50, version=version+1 WHERE id=1 AND version=5

succeeds only once because the version changes after the first execution.

3. Anti‑Repeat Token

Clients obtain a token (e.g., UUID) from the server, store it in Redis with a short TTL, and include it in request headers. A Lua script atomically checks the token’s existence and deletes it, ensuring a single successful execution.

Lua script used:

if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end

4. Downstream Unique Sequence Number

The downstream service generates a short‑lived unique sequence number (or order number) and sends it with the request. The upstream service stores the combination of sequence number and authentication ID in Redis; duplicate detection is performed by checking the key.

Below is a complete Spring Boot demonstration implementing the anti‑repeat token approach.

Project Setup (Maven)

<?xml version="1.0" encoding="UTF-8"?>
<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>
    <groupId>mydlq.club</groupId>
    <artifactId>springboot-idempotent-token</artifactId>
    <version>0.0.1</version>
    <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>
    <build>...</build>
</project>

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

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:";

    /** Generate a token and store it in Redis for 5 minutes */
    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;
    }

    /** Validate token atomically using Lua */
    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<Long> 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;
    }
}

Controller for Token Retrieval and Idempotent Test

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 ? "正常调用" : "重复调用";
    }
}

Spring Boot Application Entry

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

JUnit Test Demonstrating Idempotency

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("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("Attempt {}: {}", i, result);
            if (i == 1) {
                Assert.assertEquals("正常调用", result);
            } else {
                Assert.assertEquals("重复调用", result);
            }
        }
    }
}

In summary, idempotency is crucial for services involving financial transactions or any operation that must not be executed multiple times unintentionally. Choose the appropriate strategy based on business requirements: unique primary keys for creation, optimistic locking for updates, downstream sequence numbers for inter‑service calls, or anti‑repeat tokens for generic scenarios.

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.

JavadatabaseredisSpring BootIdempotencyREST APIToken
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.