Redis

The three beans, RedisIdempotencyStore.CODEC, why it is not autoconfigured, and what it cannot do.

The Redis store uses Lettuce. The application owns the client and the connection, which is why the beans declare their shutdown methods.

  1. The client.

    @Bean(destroyMethod = "shutdown")
    public RedisClient redisClient() {
        return RedisClient.create("redis://localhost:6379");
    }
    Java
  2. The connection, opened with the store’s own codec.

    RedisIdempotencyStore.CODEC is what keeps stored bodies binary-safe. One thread-safe connection can serve the store.

    @Bean(destroyMethod = "close")
    public StatefulRedisConnection<String, byte[]> idempotencyRedisConnection(RedisClient client) {
        return client.connect(RedisIdempotencyStore.CODEC);
    }
    Java
  3. The store.

    @Bean
    public IdempotencyStore idempotencyStore(StatefulRedisConnection<String, byte[]> connection) {
        RedisIdempotencyStoreConfig config = RedisIdempotencyStoreConfig.builder()
                .keyPrefix("payments:idempotency:")
                .build();
    
        return new RedisIdempotencyStore(connection, config);
    }
    Java

Tuning the store

RedisIdempotencyStoreConfig carries more than the key prefix. The defaults are sound and most applications change none of them. Two concerns are worth understanding before you do: what a waiting caller costs under load, and what survives a failover.

  • keyPrefix - namespace for every key the store writes. Defaults to idempotency4j:.
  • pollInterval - the first interval of a waiting caller’s jittered backoff, which doubles up to 1s. Defaults to 50ms.
  • retentionGrace - how much longer the Redis key’s own TTL outlives the record’s logical expiry, as a backstop when purge does not run. Defaults to 1 hour.
  • purgeBatchSize - the SCAN COUNT hint for each purge page. Defaults to 500.
  • maxPurgePagesPerCall - pages one purge run will scan before stopping. Defaults to 100.
  • replicaAcknowledgement - the Redis WAIT policy applied after each mutation. Disabled by default.

pollInterval is the one to know about. Redis has no way to block on the specific condition the store waits for, so a caller waiting out someone else’s lease polls, starting near 50ms and backing off towards 1s. A duplicate that arrives mid-flight can therefore see up to its current interval of extra latency before it sees the completion. Lowering it sharpens the early polls at the cost of more round trips per waiting caller.

purgeBatchSize and maxPurgePagesPerCall bound a single purge run, which is what keeps the SCAN from becoming a long-running command on a large keyspace. A run that hits the page limit stops and resumes on the next scheduled purge, so the two together cap how much work one run does rather than how much gets purged overall.

Durability across a failover

Redis replication is asynchronous. A completion the primary acknowledged may not have reached a replica yet, so losing the primary at that moment loses the record - and the next duplicate re-executes.

Where that matters, require replica acknowledgement:

RedisIdempotencyStoreConfig config = RedisIdempotencyStoreConfig.builder()
        .keyPrefix("payments:idempotency:")
        .replicaAcknowledgement(RedisReplicaAcknowledgement.require(1, Duration.ofMillis(200)))
        .build();
Java

This applies Redis WAIT after each successful mutation, so a write is not treated as done until the requested number of replicas has it. It costs latency on every mutation and it is disabled by default, because paying it unconditionally would be the wrong default for the majority of deployments that do not run replicas at all.

It narrows the window rather than closing it. WAIT reports how many replicas acknowledged; it is not a distributed transaction. If losing a record is unacceptable, use JDBC.

Operating it

Use Redis 7 or newer, and choose an application-specific key prefix.

Why it is not autoconfigured

The Redis store needs a StatefulRedisConnection<String, byte[]> - raw Lettuce with a byte-array codec - rather than the RedisConnectionFactory Spring Boot produces.

Bridging the two would mean either reaching into Spring Data Redis internals or reimplementing Boot’s URL, Sentinel, SSL, and pooling handling and then running two clients with two lifecycles. Both are worse than the three beans above.

This is the honest architectural answer rather than a gap waiting to be filled: three explicit beans you can read beat a bridge that breaks on a Spring Data Redis upgrade.

What it cannot do

Both are listed on limitations.

Security

Use TLS and ACLs, and restrict the ACL to the configured key prefix. The store holds whatever the adapter hands it, which over HTTP means full response bodies. See security.