Understanding Idempotency and Implementing It in Spring Boot with Redis

This article explains the concept of idempotency, why it is needed for reliable API design, compares HTTP method idempotence, and presents four practical backend solutions—including unique primary keys, optimistic locking, anti‑repeat tokens, and downstream sequence numbers—accompanied by Spring Boot and Redis code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Understanding Idempotency and Implementing It in Spring Boot with Redis

What is Idempotency?

Idempotent is a mathematical and computer‑science concept where applying an operation twice yields the same result as applying it once.

In programming, an idempotent operation produces the same effect no matter how many times it is executed.

An idempotent function can be called repeatedly with the same parameters and will always return the same result without changing system state.

What is API Idempotency?

HTTP/1.1 defines idempotency: a request to a resource should have the same effect whether it is performed once or multiple times (excluding network time‑outs). The first request may have side effects, but subsequent identical requests must not cause additional side effects.

Why Do We Need Idempotency?

Repeated submissions can occur in several scenarios:

Frontend duplicate form submission : Users may click the submit button repeatedly when a network glitch prevents immediate feedback.

Malicious repeated voting : Users can vote multiple times, corrupting the result.

Interface timeout retries : HTTP clients often retry on timeout, causing duplicate requests.

Message re‑consumption : Message‑queue consumers may process the same message more than once if acknowledgments fail.

Idempotency’s biggest advantage is that it prevents unknown problems caused by retries.

Impact of Introducing Idempotency

Idempotency simplifies client logic and prevents duplicate submissions, but it adds complexity and cost on the server side:

Parallel operations may need to be serialized, reducing throughput.

Additional business logic is required to enforce idempotency.

Therefore, you should evaluate whether idempotency is truly necessary for a given API.

Idempotency in RESTful APIs

Among common HTTP methods, some are idempotent and some are not: Idempotent (e.g., GET, PUT, DELETE) x Not idempotent (e.g., POST) - May be idempotent depending on business logic (e.g., PATCH)

Solution 1: Database Unique Primary Key

Using a unique primary key (often a distributed ID) guarantees that an insert operation is idempotent because the table can contain only one row with that key.

Applicable Operations

Insert

Delete

Constraints

A globally unique primary‑key ID must be generated.

Main Flow

Client sends a create request to the server.

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

If the insert succeeds, the request is unique; if a duplicate‑key exception occurs, the record already exists and the server returns an error.

Solution 2: Database Optimistic Lock

Optimistic locking works for update operations by adding a version column to the table. The version value is used as a condition in the UPDATE statement.

Applicable Operations

Update

Constraints

An extra version field must be added to the table.

Example

Assume a table with a version column. When updating, the client sends the expected version (e.g., version=5) and the SQL statement:

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

If the row’s version has already changed, the WHERE clause matches no rows, making the operation effectively idempotent.

Solution 3: Anti‑Repeat Token

Clients obtain a global Token (e.g., UUID) from the server and include it in request headers. The server stores Token as a key in Redis with an associated value (often user info). Validation consists of a Lua script that atomically checks the key/value and deletes the key if it matches.

Applicable Operations

Insert

Update

Delete

Constraints

A globally unique token string must be generated.

Redis (or similar) is required for atomic verification.

Main Flow

Client requests a token; server generates and stores it in Redis with a short TTL.

Client includes the token in request headers.

Server runs a Lua script that checks GET tokenKey equals the expected value and deletes the key atomically.

If the script returns a non‑zero result, the request is accepted; otherwise it is rejected as a duplicate.

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

Solution 4: Downstream Unique Sequence Number

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

Applicable Operations

Insert

Update

Delete

Constraints

The downstream must provide a unique sequence number.

Redis is required for key/value verification.

Main Flow

Downstream generates a distributed ID and sends it with its authentication ID.

Upstream validates the pair, builds a Redis Key, and checks if it exists.

If the key exists, the request is a duplicate and an error is returned.

If the key does not exist, the pair is stored in Redis (with TTL) and the business logic proceeds.

Implementation Example (Spring Boot + Redis)

1. Maven Dependencies

<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>
    <!-- commons-pool2 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2. Redis Connection 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

@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 */
    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 using a Lua script for atomic get‑and‑del */
    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 a token */
    @GetMapping("/token")
    public String getToken() {
        String userInfo = "mydlq"; // simulated 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 ? "Normal call" : "Duplicate call";
    }
}

Conclusion

Idempotency is crucial for services involving money, such as payments and orders. Choose the appropriate strategy based on the business scenario: unique primary‑key for insert‑only operations, optimistic lock for updates, downstream sequence numbers for cross‑service calls, or anti‑repeat tokens with Redis for generic duplicate‑submission protection.

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.

JavaSpring BootREST APIToken
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

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.