Quickstart

The starter, a store you already run and one annotation - idempotency4j running in five minutes.

  1. 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.

    Build tool
    Store
    Maven
    <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>

    Autoconfigured from a single DataSource, and the only store that can join your transaction. JDBC.

    More than one module? Import idempotency-bom once 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.

  2. Create the table, if you took the JDBC store.

    With idempotency-jdbc on the classpath and a single DataSource in 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 the idempotency-jdbc jar.

    MySQL

    Point Flyway, Liquibase or your own migration at idempotency-schema-mysql.sql, shipped inside the idempotency-jdbc jar.

    H2

    Nothing to do. idempotency.jdbc.initialize-schema defaults to embedded, which creates the table on an embedded database and only there.

    See JDBC for the full story, including initialize-schema: always.

  3. 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.
    }
    Java

    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));
    }
    Java

    Without Spring

    The engine is a plain Java object you call directly. It takes a store and a ScheduledExecutorService it runs heartbeats on:

    IdempotencyEngine engine = new IdempotencyEngine(store, scheduler);
    Java

    See the engine.

  4. 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"}'
    Shell

    The second response carries Idempotent-Replayed: true and the handler did not run.