DEV Community

Cover image for I Built a Spring Boot Starter to Handle Duplicate API Requests
IROSH PERERA
IROSH PERERA

Posted on

I Built a Spring Boot Starter to Handle Duplicate API Requests

A client sends a request to create an order. The server processes it, but the connection drops before the response reaches the client.

The client retries. How can the API recognise that retry and avoid creating another order?

This is the problem my open-source Spring Boot Idempotency Starter aims to address.

What is idempotency?

For an API operation, idempotency means that repeating the same logical request should not produce additional side effects.

An idempotency key lets the client identify that logical request. It should reuse the same key when retrying and generate a new key for a new operation.

What the starter provides

The starter supports:

  • An @Idempotent annotation for protected endpoints.
  • An Idempotency-Key request header.
  • Request body hash validation.
  • Caching and replaying completed responses.
  • In-memory and Redis storage.
  • Configurable record expiry and processing timeout.
  • Spring Boot auto-configuration.

Here is how to get started.

1. Add the Maven dependency

Requirements: Java 17+, Spring Boot 3.x, and Maven 3.8+.

<dependency>
    <groupId>io.github.iroshperera</groupId>
    <artifactId>spring-boot-idempotency-starter</artifactId>
    <version>0.1.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

2. Annotate an endpoint

Add @Idempotent to the endpoint you want to protect:

import lk.irosh.idempotency.annotation.Idempotent;

@Idempotent
@PostMapping("/orders")
public OrderResponse createOrder(
        @RequestBody OrderRequest request
) {
    return orderService.create(request);
}
Enter fullscreen mode Exit fullscreen mode

This snippet belongs inside your controller. OrderRequest, OrderResponse, and orderService represent your application's own types and service.

3. Configure the starter

For a local example, add the following to application.yml:

idempotency:
  enabled: true
  storage: memory
  required: true
  header-name: Idempotency-Key
  default-expiry: 24h
  processing-timeout: 5m
  cache-response: true
  reject-different-request-hash: true
Enter fullscreen mode Exit fullscreen mode

This configuration requires a key, enables response caching, and rejects reuse of a key with a different request body.

4. Send a request with a key

Assuming your endpoint is available at /orders and accepts the example payload:

curl -X POST http://localhost:8080/orders \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-request-001" \
  -d '{"item":"Laptop","quantity":1}'
Enter fullscreen mode Exit fullscreen mode

Retry the same request with the same key and body.

The documented behaviour is:

Scenario Behaviour
First request Executes the controller and saves the response
Retry after completion, with the same key and body Returns the saved response without executing the controller again
Same key with a different body Returns 409 Conflict
Missing key when a key is required Returns 400 Bad Request

Use a fresh key when creating a genuinely new order.

Choosing a storage option

In-memory storage is a starting point for development, testing, and single-instance applications. Its records are local to the running application and do not survive a restart.

For shared records across application instances, the starter also provides Redis storage:

idempotency:
  storage: redis

spring:
  data:
    redis:
      host: localhost
      port: 6379
Enter fullscreen mode Exit fullscreen mode

Merge these settings into your configuration and make sure Redis is running.

Things to consider before production use

Idempotency records have a limited lifetime. Once a record expires, you should not assume that an old key will still prevent duplicate processing.

Shared storage is also only one part of a distributed solution. Concurrent requests, failures during processing, and the relationship between business transactions and stored responses need careful testing.

For critical operations, keep appropriate database constraints and business-level safeguards in place. An idempotency layer should not be treated as a blanket guarantee of exactly-once execution.

What’s next?

The project roadmap includes:

  • JDBC and PostgreSQL storage.
  • MongoDB storage.
  • Distributed locking improvements.
  • Metrics and monitoring.
  • Additional integration tests.

Try it and share your feedback

The project is open source under the MIT License.

👉 View Spring Boot Idempotency Starter on GitHub

Feedback, bug reports, and contributions are welcome—especially around concurrency and failure scenarios.

How do you handle duplicate requests in your Spring Boot applications?

Top comments (0)