Run once. Same result on every retry. A lightweight Spring Java library that makes any method or service safe to retry — not just HTTP APIs. Duplicate calls return the cached result; concurrent callers wait for the first run instead of racing.
Annotate a controller, a @Service, or a batch job. Or call IdempotentService directly. Pick Redis, DynamoDB, NATS KV, or JDBC — or start with the built-in in-memory store.
@Idempotent(key = "#order.id", duration = "30m")
@PostMapping("/orders")
public Receipt place(@RequestBody Order order) {
return paymentGateway.charge(order); // runs once, even on a retry storm
}No bespoke locks. No “did this already happen?” checks. No duplicate work.
In a distributed system, the same request can arrive twice — or twenty times. Networks time out, clients retry, load balancers replay, queues redeliver. Without idempotency, you ship the order twice, charge the card again, or corrupt state.
Idempotent ties every retry to one logical operation: same key → same outcome. It claims the key atomically, runs your code once, caches the result, and coordinates any concurrent callers — across Redis, DynamoDB, NATS KV, or your SQL database.
You add a dependency and an annotation. The library handles the hard parts.
| Two entry points, one engine | @Idempotent on any Spring method, or IdempotentService.execute(...) from @Service classes, batch jobs, or message consumers. |
| Atomic key claims | Native primitives on every backend (SET NX, conditional PutItem, kv.create, INSERT) — two callers cannot both think they were first. |
| Self-healing expiry | Per-entry TTL, native backend TTL where available, lazy delete on read, scheduled cleanup for SQL. No zombie keys. |
| Keys your way | Client header (X-Idempotency-Key) or server-side SpEL (#user.id), with optional SHA-256 hashing. |
| Honest semantics | Domain exceptions propagate as-is. Null/void results are cached. Non-2xx ResponseEntity is removed so the client can retry. |
Client sends the same request twice (timeout, retry, replay):
POST /orders X-Idempotency-Key: 1f3c1a93-…-3bd4
POST /orders X-Idempotency-Key: 1f3c1a93-…-3bd4 ← duplicate| Scenario | What happens |
|---|---|
| First call | Claim key (IN_PROGRESS) → run your code → store COMPLETED |
| Duplicate while in progress | Poll with exponential backoff → return the same response |
| Duplicate after success | Cache hit — your method body does not run |
| Operation throws | Entry removed, exception propagates, retry allowed |
| Entry expired | Removed on next read; a new caller can claim the key |
Set
durationlonger than your slowest operation, so an entry is never evicted mid-flight.
Same engine from any code — useful in @Service classes, consumers, or jobs:
return idempotentService.execute(
"ord_42", "create-order", Order.class,
() -> orderService.create(request),
Duration.ofMinutes(30));In-memory is the default. Pick a persistent backend for production:
| Module | Best for | Key primitive |
|---|---|---|
| Redis | Sub-ms reads, native TTL, already in your stack | SET NX / SET XX |
| DynamoDB | AWS-native, managed scale, table TTL | Conditional PutItem |
| NATS KV | KV co-located with your JetStream cluster | create + conditional put |
| RDS / JDBC | Postgres, MySQL, MariaDB, or H2 you already run | INSERT + scheduled cleanup |
All four share the same serialization, expiry, and state machine — swap backends without changing behavior.
Add a storage module (it pulls in idempotent-core):
<dependency>
<groupId>io.github.arun0009</groupId>
<artifactId>idempotent-redis</artifactId> <!-- or -dynamo, -nats, -rds -->
<version>${idempotent.version}</version>
</dependency>For @Idempotent, also add spring-boot-starter-aop. IdempotentService works without it.
API reference, configuration, and custom stores: idempotent-core.
Issues and pull requests welcome at github.com/arun0009/idempotent. Released under the MIT License.
