Ensuring API Idempotency: 4 Proven Strategies with SpringBoot & Redis

This article explains why API idempotency is crucial in real‑world systems, outlines which operations need it, discusses the trade‑offs of adding idempotency, and presents four practical implementation patterns—including unique primary keys, optimistic locking, anti‑duplicate tokens, and downstream sequence numbers—complete with SpringBoot code examples.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Ensuring API Idempotency: 4 Proven Strategies with SpringBoot & Redis

Idempotency Introduction

Idempotency is a frequent interview topic and a real‑world problem; duplicate message consumption caused issues in a past project, prompting this guide on ensuring API idempotency with four selectable solutions.

1. What is API Idempotency

Idempotency means that multiple identical requests produce the same result without side effects.

For example, a payment may succeed but the response fails due to a network error; a user clicks again, causing a second charge and duplicate transaction records.

2. Why Implement Idempotency

Frontend repeated form submissions due to missing success feedback.

Malicious repeated actions such as vote spamming.

Client‑side timeout retries that resend the request.

Message brokers delivering the same message multiple times.

The biggest benefit is preventing unknown issues caused by retries.

3. Operations That Need Idempotency

Among CRUD operations, creation and update are most vulnerable.

Creation

Repeated submissions can cause duplicate charges.

Deletion

Deleting once or multiple times yields the same final state (the data is gone), though response codes may differ.

Update

Setting a field to a fixed value is idempotent; incrementing a field is not.

Query

Read‑only queries are naturally idempotent.

4. Impact of Adding Idempotency

Idempotency simplifies client logic but adds server‑side complexity and cost:

Parallel operations may need to be serialized, reducing throughput.

Additional business logic increases code complexity.

Introduce idempotency only when the business scenario truly requires it.

Common Solutions for Implementing Idempotency

Solution 1: Database Unique Primary Key

Description

Leverages the unique constraint of a primary key, suitable for insert operations to ensure only one record with the same key exists.

Use a distributed ID (not an auto‑increment key) to guarantee global uniqueness in a distributed environment.

Applicable Operations

Insert

Delete

Usage Constraints

Requires generation of a globally unique ID.

Main Flow

Client sends a create request to the server.

Server generates a distributed ID, uses it as the primary key, and executes the INSERT SQL.

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

Solution 2: Database Optimistic Lock

Description

Suitable for update operations. Add a version column to the table and include the version in the WHERE clause; the update succeeds only if the version matches.

Applicable Operations

Update

Usage Constraints

Requires an extra version column in the business table.

Example

Assume a table with columns id, name, price, version. The update statement includes the version condition:

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

If the row with id=1 and version=5 no longer exists, the statement has no effect, guaranteeing idempotent updates.

Solution 3: Anti‑Duplicate Token

Description

Generate a global token (e.g., UUID) and store it in Redis with associated user data. The client includes the token in request headers; the server validates and deletes the token atomically using a Lua script.

Applicable Operations

Insert

Update

Delete

Usage Constraints

Requires generation of a globally unique token string.

Requires Redis for token storage and validation.

Main Flow

Server provides an endpoint to obtain a token.

Client calls the endpoint and receives the token.

Server stores the token as a Redis key (with expiration).

Client includes the token in request headers.

Server retrieves the token from Redis, runs a Lua script to check the value and delete the key atomically.

If validation succeeds, the business logic proceeds; otherwise, a duplicate‑submission error is returned.

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

Solution 4: Downstream Unique Sequence Number

Description

The downstream service generates a short‑lived unique sequence number (e.g., order ID) and sends it with the request. The upstream service composes a Redis key from the sequence number and authentication ID to detect duplicates.

Applicable Operations

Insert

Update

Delete

Usage Constraints

Downstream must provide a unique sequence number.

Redis is required for duplicate detection.

Main Flow

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

Upstream validates the presence of both parameters.

Upstream checks Redis for a key composed of the sequence number and authentication ID; if it exists, a duplicate error is returned; otherwise, the key/value is stored (with expiration) and the business logic proceeds.

The Redis key should have an expiration time; otherwise, stale keys may accumulate and degrade Redis performance.

Idempotent Code Example

The following demonstrates the anti‑duplicate token approach using SpringBoot and Redis.

1. Add 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>
        <!-- springboot web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springboot data redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</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

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

@Slf4j
@Service
public class TokenUtilService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";

    /** Create token and store in Redis */
    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 */
    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;
    }
}

4. Test Controller

@Slf4j
@RestController
public class TokenController {

    @Autowired
    private TokenUtilService tokenService;

    /** Get token */
    @GetMapping("/token")
    public String getToken() {
        String userInfo = "mydlq"; // mock user info
        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 Class

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

6. Test Class

@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++) {
            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 succeeds and subsequent calls are identified as duplicates.

Conclusion

Use a unique primary key for scenarios like order creation.

Apply optimistic locking for update‑heavy operations.

Downstream‑generated sequence numbers work well for upstream services.

For front‑end repeated submissions or cases without a natural unique ID, combine a token with Redis for quick idempotency enforcement.

Choose the method that matches your business logic, handle each detail carefully, and you’ll keep your system reliable.

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.

JavaRedisAPIidempotencyspringboottoken
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

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.