Implementing Idempotent APIs in SpringBoot

This article explains the concept of idempotency, why it matters for HTTP APIs, the impact on system design, RESTful method characteristics, and compares four practical implementation strategies—including database primary keys, optimistic locking, Redis token, and downstream sequence numbers—followed by a complete SpringBoot example with code and tests.

IT Niuke
IT Niuke
IT Niuke
Implementing Idempotent APIs in SpringBoot

1. What is Idempotency

Idempotency is a concept in mathematics and computing 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.

An idempotent function or method can be called repeatedly with the same parameters and always return the same result without altering system state.

2. What is API Idempotency

HTTP/1.1 defines idempotency for requests: multiple identical requests to a resource should have the same effect as a single request (excluding network time‑outs). The first request may cause side effects; subsequent identical requests must not change the resource.

3. Why Implement Idempotency

Repeated submissions can occur in several scenarios:

Front‑end duplicate form submission: Network glitches may prevent the client from receiving a success response, leading the user to click the submit button repeatedly.

Malicious repeated actions (e.g., vote spamming): Users may intentionally submit the same request many times, corrupting results.

Interface timeout retries: HTTP clients often retry on timeout, causing multiple submissions.

Message re‑consumption: MQ consumers may process the same message more than once if acknowledgement fails.

Idempotency prevents these problems by guaranteeing that repeated executions have no additional impact.

4. Impact of Introducing Idempotency

While idempotency simplifies client logic, it adds server‑side complexity and cost:

Parallel operations may need to be serialized, reducing throughput.

Additional business logic is required to enforce idempotency, complicating the code base.

Therefore, developers should evaluate whether idempotency is truly needed for a given interface.

5. Idempotency of RESTful API Methods

According to the RESTful recommendation, HTTP methods have the following idempotency characteristics:

GET, PUT, DELETE, HEAD, OPTIONS – ✓ idempotent

POST – ✗ not idempotent

PATCH – – may be idempotent depending on business logic

6. How to Implement Idempotency

Solution 1: Database Unique Primary Key

Description: Use the unique constraint of a primary key to guarantee that an insert (or delete) operation cannot be performed twice. The primary key should be a globally unique ID (e.g., a distributed ID) rather than an auto‑increment column.

Applicable operations: Insert, Delete.

Limitation: Requires generation of a globally unique primary key.

Main flow:

Client sends a create request.

Server generates a distributed ID, uses it as the primary key, and attempts to insert.

If the insert succeeds, the request is first‑time; if a duplicate‑key exception occurs, the request is a repeat.

Solution 2: Database Optimistic Lock

Description: Add a version column to the table. Each update includes the current version in the WHERE clause; the update increments the version. If the version does not match, the update fails, preventing duplicate execution.

Applicable operation: Update.

Limitation: Requires an extra version field in the table.

Example SQL:

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

The WHERE clause ensures that only the expected version is updated; a repeated execution will affect zero rows, preserving idempotency.

Solution 3: Anti‑Replay Token (Redis)

Description: The client first obtains a globally unique token from the server (stored in Redis). Subsequent requests include the token (preferably in HTTP headers). The server checks Redis for the token‑value pair; if present, it deletes the key and proceeds, otherwise it returns a duplicate‑submission error.

Applicable operations: Insert, Update, Delete.

Limitations: Requires generation of a globally unique token and a Redis instance for validation.

Main flow:

Server provides an endpoint to generate a token.

Client calls the endpoint and receives the token.

Token is stored in Redis with a short TTL and the associated user information as value.

Client includes the token in request headers.

Server executes a Lua script that atomically checks the token/value and deletes the key if they match.

In concurrent scenarios, the Redis lookup and delete must be atomic; this can be achieved with a distributed lock or a Lua script.

Solution 4: Downstream‑Provided Unique Sequence Number

Description: The downstream service generates a short‑lived unique sequence number (or order number) and passes it together with its authentication ID to the upstream service. The upstream service composes a Redis key from these values and checks for existence.

Applicable operations: Insert, Update, Delete.

Limitation: Requires the downstream to supply a unique sequence number and Redis for validation.

Main flow:

Downstream generates a distributed ID as the sequence number and calls the upstream API, attaching the sequence number and its auth ID.

Upstream validates the presence of both fields.

If the composed Redis key exists, the request is a duplicate; otherwise the key is stored and the business logic proceeds.

7. Idempotent API Example (Token Scheme)

The following steps implement the token‑based solution in a SpringBoot project.

1. Maven Dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <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>
    <name>springboot-idempotent-token</name>
    <description>Idempotent Demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Data Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Commons Pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. Redis Configuration (application.yml)

spring:
  redis:
    ssl: false
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1000
    password:
    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;

    /** Token key prefix in Redis */
    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 a Lua script */
    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("Validate token={}, key={}, value={} success", token, key, value);
            return true;
        }
        log.info("Validate token={}, key={}, value={} failed", 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;

    /** Get a token */
    @GetMapping("/token")
    public String getToken() {
        String userInfo = "mydlq";
        return tokenService.generateToken(userInfo);
    }

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

5. SpringBoot 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);
    }
}

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

        // Obtain token
        String token = mockMvc.perform(MockMvcRequestBuilders.get("/token")
                .accept(MediaType.TEXT_HTML))
                .andReturn()
                .getResponse().getContentAsString();
        log.info("Token: {}", token);

        // Call test endpoint five times
        for (int i = 1; i <= 5; i++) {
            log.info("Call {}", i);
            String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
                    .header("token", token)
                    .accept(MediaType.TEXT_HTML))
                    .andReturn().getResponse().getContentAsString();
            log.info(result);
            if (i == 1) {
                Assert.assertEquals("正常调用", result);
            } else {
                Assert.assertEquals("重复调用", result);
            }
        }
    }
}

Test output shows the first call returns “正常调用” (normal call) and subsequent calls return “重复调用” (duplicate call), confirming the idempotent behavior.

8. Final Summary

Idempotency is essential for services such as payment or order processing. Choose the implementation that matches the business scenario:

Use the unique‑primary‑key scheme for operations that naturally have a unique identifier (e.g., order creation).

Use the optimistic‑lock scheme for update‑centric scenarios.

Use the downstream‑sequence‑number scheme for cross‑service calls.

Use the token‑plus‑Redis scheme for front‑end duplicate submissions or cases without a natural unique ID.

Understanding the specific requirements and carefully designing each step ensures reliable idempotent APIs.

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.

backendJavaRedisidempotencyspringboot
IT Niuke
Written by

IT Niuke

Focused on IT technology sharing, original and innovative content. IT Niuke, we grow 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.