AvoOnce is a lightweight, framework-agnostic distributed idempotency library for Java. It solves the "exactly-once" execution challenge across distributed services and microservices by enforcing a strict state machine, in-flight concurrent execution locking, payload tamper protection, and byte-perfect HTTP response caching based on the IETF Idempotency-Key specification.
- Selective
@IdempotentProtection: Explicit, annotation-driven protection on your endpoints (@Idempotent). Unannotated endpoints (e.g. health checks, metrics, reads) bypass the filter completely with zero overhead. - Multi-Framework Support: First-class integrations for Spring Boot 4.0+ (Servlet Filter via
OncePerRequestFilter), Quarkus 3.12+, Dropwizard 4.0+, and any Jakarta EE 10 / JAX-RS 3.1+ framework (Jersey 3+, RESTEasy 6+ via@NameBindingContainerRequestFilter). - Byte-Perfect HTTP Caching & Replay: Captures exact status codes, headers, and raw response bytes, replaying responses without invoking business logic or database queries.
- In-Flight Concurrency Locking: Prevents concurrent duplicate requests from double-executing. Simultaneous in-flight requests with the same key receive an immediate
409 Conflict. - Payload Tamper Protection: Computes a cryptographic SHA-256 hash of the request body to detect and reject modified payloads reusing an existing key (
422 Unprocessable Entity). - Fault-Tolerant Retry Handling: Server errors (
5xx) automatically transition the idempotency record toFAILED, allowing clients to safely retry after transient failures. - Pluggable Storage Backends (SPI): Clean Service Provider Interface with built-in production backends:
- Caffeine: Ultra-fast, single-node in-memory store.
- JDBC: Distributed store supporting PostgreSQL, MySQL, H2, Oracle, MariaDB, and SQL Server with automatic table creation (
auto-ddl) and scheduled background eviction. - Redis: High-throughput distributed store with native TTL expiration.
- Standards-Compliant: Conforms to the IETF
Idempotency-KeyHTTP Header draft specification.
| Challenge | Distributed Locks Alone (e.g. ShedLock / Redis Lock) | AvoOnce Idempotency Engine |
|---|---|---|
| Dropped Responses | ❌ Lock releases after execution; client retry fails or re-executes | ✅ Caches & replays exact status, headers, and body bytes |
| Concurrent Duplicates | ✅ Returns 409 Conflict during execution, cached result on retry |
|
| Payload Mutation | ❌ Key reused with different payload executes or corrupts state | ✅ SHA-256 body hash validation rejects tampered requests (422) |
| Framework Flexibility | ❌ Coupled to specific frameworks or annotations | ✅ Framework-agnostic core SPI + Spring Boot, Quarkus, Dropwizard & JAX-RS adapters |
| Selective Scope | ✅ Clean @Idempotent annotation on methods or controller classes |
graph TD
Client((Client)) -->|"HTTP Request with<br/>Idempotency-Key"| Web[Web Layer]
subgraph Framework Integrations
Web -->|"@Idempotent"| SB[idempotency-spring-boot-starter<br/>Spring Boot MVC]
Web -->|"@Idempotent"| JAX[idempotency-jaxrs<br/>Quarkus / Dropwizard / Jersey / RESTEasy]
end
SB --> Core[idempotency-core<br/>IdempotencyManager & State Machine]
JAX --> Core
subgraph Storage Backends SPI
Core -->|SPI| Caff[idempotency-caffeine<br/>In-Memory]
Core -->|SPI| JDBC[idempotency-jdbc<br/>PostgreSQL / MySQL / H2 / Oracle]
Core -->|SPI| Red[idempotency-redis<br/>Distributed Redis]
end
AvoOnce is currently hosted on GitHub Packages, which requires authentication. This will be changed in the future to Maven Central.
GitHub Packages Authentication Required
- Generate a GitHub Personal Access Token with
read:packagesscope. - Add the token to your
~/.m2/settings.xml:
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_PAT</password>
</server>
</servers>- Add the repository to your
pom.xml:
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/ravocode/AvoOnce</url>
</repository>
</repositories><!-- Spring Boot Starter -->
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Choose a Storage Backend -->
<!-- Option A: In-Memory (Caffeine) -->
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-caffeine</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Option B: Relational DB (JDBC) -->
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-jdbc</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Option C: Distributed Redis -->
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-redis</artifactId>
<version>1.0.0</version>
</dependency>Annotate the target controller method or class with @Idempotent:
import io.github.ravocode.avoonce.spring.annotation.Idempotent;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@PostMapping
@Idempotent // Protected by AvoOnce
public ResponseEntity<PaymentResponse> createPayment(@RequestBody PaymentRequest req) {
PaymentResponse response = paymentService.process(req);
return ResponseEntity.status(201).body(response);
}
@PostMapping("/unprotected")
// Unannotated: bypasses idempotency filter even if Idempotency-Key is sent
public ResponseEntity<PaymentResponse> unprotected(@RequestBody PaymentRequest req) {
return ResponseEntity.ok(paymentService.process(req));
}
}Include the Idempotency-Key header in client requests:
curl -i -X POST http://localhost:8080/api/payments \
-H "Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Content-Type: application/json" \
-d '{"amount": 99.99, "accountId": "acc-456"}'If the client retries with the same key, AvoOnce intercepts the call, bypasses the controller, and replays the cached HTTP response instantly!
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-jaxrs</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.github.ravocode.avoonce</groupId>
<artifactId>idempotency-caffeine</artifactId>
<version>1.0.0</version>
</dependency>@ApplicationScoped
public class IdempotencyProducer {
@Produces
@Singleton
public IdempotencyRepository idempotencyRepository() {
return new CaffeineIdempotencyRepository(new IdempotencyConfig());
}
}@Override
public void run(MyConfiguration config, Environment environment) {
IdempotencyRepository repository = new CaffeineIdempotencyRepository(new IdempotencyConfig());
environment.jersey().register(new IdempotencyContainerFilter(repository));
}import io.github.ravocode.avoonce.jaxrs.Idempotent;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
@Path("/api/payments")
public class PaymentResource {
@POST
@Idempotent // Name-bound JAX-RS filter protection
@Consumes("application/json")
@Produces("application/json")
public Response processPayment(PaymentRequest request) {
return Response.status(201).entity(paymentService.process(request)).build();
}
}Customize starter properties in application.yml or application.properties:
avoonce:
idempotency:
# Storage engine: "auto", "caffeine", "jdbc", or "redis"
store: "auto"
# HTTP header name
header-name: "Idempotency-Key"
# Time-To-Live for cached responses
ttl: 1
ttl-unit: HOURS
# Lock timeout for active requests in progress
lock-timeout: 2
lock-timeout-unit: MINUTES
# SHA-256 payload tampering validation
hash-body: true
# Reject requests to @Idempotent endpoints missing the header (HTTP 400)
enforce: false
# Servlet filter enablement
filter:
enabled: true
# JDBC specific configuration
jdbc:
auto-ddl: true
eviction:
enabled: true
interval-ms: 3600000| Module | Description | Documentation |
|---|---|---|
idempotency-core |
Core state machine, SHA-256 hasher, response wrappers, and storage SPI | README |
idempotency-caffeine |
Fast in-memory storage implementation backed by Caffeine | README |
idempotency-jdbc |
Distributed relational database storage (PostgreSQL, MySQL, H2, Oracle, MariaDB, SQL Server) | README |
idempotency-redis |
Distributed Redis storage with native TTL management | README |
idempotency-spring-boot-starter |
Spring Boot 4.0+ auto-configuration and @Idempotent Servlet Filter |
README |
idempotency-spring-boot-sample |
Spring Boot reference application demonstrating Caffeine and JDBC backends | README |
idempotency-jaxrs |
Jakarta EE 10 / JAX-RS 3.1+ integration with @Idempotent name-binding |
README |
idempotency-quarkus-sample |
Quarkus 3.12 reference application demonstrating CDI integration | README |
idempotency-acceptance-tests |
End-to-end acceptance test suite verifying concurrent locks, replays, and failures | Acceptance Tests |
This project is licensed under the MIT License.
