JDBC

Autoconfiguration from a DataSource, initialize-schema, and pointing a migration tool at the shipped schema.

Nothing to declare: with idempotency-jdbc on the classpath and a DataSource in the context, the store is built for you, complete with the TransactionAwareConnectionResolver that joined completion needs.

Maven

<dependency>
    <groupId>io.github.josipmusa</groupId>
    <artifactId>idempotency-jdbc</artifactId>
    <version>0.5.0</version>
</dependency>
Maven

Gradle

implementation 'io.github.josipmusa:idempotency-jdbc:0.5.0'
Gradle

Tested on PostgreSQL 16, MySQL 8.0 and H2 2.x.

The table

The table is another matter. idempotency.jdbc.initialize-schema follows the convention Spring Boot uses for Session and Quartz:

idempotency:
  jdbc:
    initialize-schema: embedded   # embedded (default) | always | never
YAML

PostgreSQL

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

Or set initialize-schema: always if you would rather the store created the table itself.

MySQL

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

Or set initialize-schema: always if you would rather the store created the table itself.

H2

Nothing to do. embedded creates the table on an embedded database and only there, so a development H2 and the store contract’s own test database both work untouched.

If the table still cannot be queried once the context has started, the starter logs a warning saying so, rather than leaving the first keyed request to fail with a 500.

Constructing it by hand

Constructing the store by hand still works, and there the initSchema flag is yours:

@Bean
public IdempotencyStore idempotencyStore(DataSource dataSource) {
    return new JdbcIdempotencyStore(dataSource, false, new TransactionAwareConnectionResolver(dataSource));
}
Java

A store bean you declare always wins, so this is also how you point the store at a second DataSource - a separate database for idempotency records - rather than the application’s primary one.

Joined completion

The JDBC store is the only shipped store that supports it. The TransactionAwareConnectionResolver runs the joined completion on the transaction-bound connection and everything else, an autonomous completion included, on a connection of its own, which is what lets the record commit with your business writes while the lease work stays outside your transaction.

Until that transaction commits, it holds the record’s row. A duplicate that arrives meanwhile does not wait for the commit beyond its own waitTimeout: it is told the record is in flight, with the holder’s remaining lease as retryAfter. Query timeouts are whole seconds, so the wait can overshoot by up to a second, or two on H2.