-
Add the starter and one store.
The starter brings the core, the AOP adapter and the HTTP filter. A store is a second dependency, and it is the one decision here: pick the one you already run.
<dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-spring-boot-starter</artifactId> <version>0.4.0</version> </dependency> <dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-jdbc</artifactId> <version>0.4.0</version> </dependency><dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-spring-boot-starter</artifactId> <version>0.4.0</version> </dependency> <dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-redis</artifactId> <version>0.4.0</version> </dependency><dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-spring-boot-starter</artifactId> <version>0.4.0</version> </dependency> <dependency> <groupId>io.github.josipmusa</groupId> <artifactId>idempotency-inmemory</artifactId> <version>0.4.0</version> </dependency>dependencies { implementation platform('io.github.josipmusa:idempotency-bom:0.4.0') implementation 'io.github.josipmusa:idempotency-spring-boot-starter' implementation 'io.github.josipmusa:idempotency-jdbc' }dependencies { implementation platform('io.github.josipmusa:idempotency-bom:0.4.0') implementation 'io.github.josipmusa:idempotency-spring-boot-starter' implementation 'io.github.josipmusa:idempotency-redis' }dependencies { implementation platform('io.github.josipmusa:idempotency-bom:0.4.0') implementation 'io.github.josipmusa:idempotency-spring-boot-starter' implementation 'io.github.josipmusa:idempotency-inmemory' }Autoconfigured from a single DataSource, and the only store that can join your transaction. JDBC.
The dependency is not enough: the client, the connection and the store are three beans you declare. Redis.
One JVM, lost on restart. Asked for by name with store-type: in-memory, never selected by auto. In-memory.
More than one module? Import
idempotency-bomonce and drop the<version>from both dependencies - the Gradle form above already does it. Two modules on two versions is the failure worth avoiding when you upgrade. -
Create the table, if you took the JDBC store.
With
idempotency-jdbcon the classpath and a singleDataSourcein the context, the store wires itself. The table does not - except on an embedded database.PostgreSQL
Point Flyway, Liquibase or your own migration at
idempotency-schema-postgresql.sql, shipped inside theidempotency-jdbcjar.MySQL
Point Flyway, Liquibase or your own migration at
idempotency-schema-mysql.sql, shipped inside theidempotency-jdbcjar.H2
Nothing to do.
idempotency.jdbc.initialize-schemadefaults toembedded, which creates the table on an embedded database and only there.See JDBC for the full story, including
initialize-schema: always. -
Annotate the work.
On a method
A method has no transport to take a key from, so name the key with a SpEL expression over the parameters:
@Idempotent(key = "#event.id()", waitTimeout = "PT0S") @KafkaListener(topics = "orders") void on(OrderPlaced event) { // Runs once per event id, however many times the broker redelivers. }waitTimeout = "PT0S"is what you want on a consumer thread: declining a redelivery is cheap, parking a consumer thread is not.On an endpoint
An HTTP request brings its own key in a header, so the annotation needs nothing:
@PostMapping("/payments") @Idempotent public ResponseEntity<Payment> createPayment(@RequestBody PaymentRequest request) { // Duplicates get the stored response replayed. // The payment provider should also receive its own idempotency key. return ResponseEntity.ok(paymentService.charge(request)); }Without Spring
The engine is a plain Java object you call directly. It takes a store and a
ScheduledExecutorServiceit runs heartbeats on:IdempotencyEngine engine = new IdempotencyEngine(store, scheduler);See the engine.
-
Check it.
Send the same request twice with the same
Idempotency-Key:curl -i -X POST localhost:8080/payments \ -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \ -H 'Content-Type: application/json' \ -d '{"amount": 100, "currency": "USD"}'The second response carries
Idempotent-Replayed: trueand the handler did not run.
Quickstart
The starter, a store you already run and one annotation - idempotency4j running in five minutes.