Skip to content
Configuration

Store Types

Store Types

Four backing stores are available. Choose based on your deployment topology and persistence requirements.


Comparison

Store Persistence Shared across nodes Production-ready Dependency
InMemory None None
Caffeine None Single-node caffeine
JDBC Durable Any JDBC DataSource
Custom Varies Varies Varies Your implementation

InMemory

ConcurrentHashMap-backed store. Zero dependencies. Data is lost on restart. Not suitable for production.

application.yml
failover:
  store:
    type: inmemory    # default — no extra config needed

Not for production

InMemory stores data only for the lifetime of the JVM process. Any restart loses all cached failover data, leaving the first few requests unprotected until new upstream calls succeed.


Caffeine

In-process store backed by the Caffeine cache library. Suitable for single-node deployments where persistence is not required.

application.yml
failover:
  store:
    type: caffeine
pom.xml
<dependency>
    <groupId>com.societegenerale.failover</groupId>
    <artifactId>failover-store-caffeine</artifactId>
    <version>3.0.0</version>
</dependency>

Caffeine handles its own eviction using the expireOn field from ReferentialPayload. Entries are evicted at their configured TTL without needing the cleanup scheduler.


JDBC

Durable, shared-state store backed by any JDBC-compatible database. The recommended production store.

application.yml
failover:
  store:
    type: jdbc
    jdbc:
      table-prefix: MYAPP_
pom.xml
<dependency>
    <groupId>com.societegenerale.failover</groupId>
    <artifactId>failover-store-jdbc</artifactId>
    <version>3.0.0</version>
</dependency>

Create the Table

create_failover_store.sql
CREATE TABLE MYAPP_FAILOVER_STORE (
    FAILOVER_NAME  VARCHAR(50)                      NOT NULL,
    FAILOVER_KEY   VARCHAR(256)                     NOT NULL,
    AS_OF          TIMESTAMP(9) WITH TIME ZONE      NOT NULL,
    EXPIRE_ON      TIMESTAMP(9) WITH TIME ZONE      NOT NULL,
    PAYLOAD        VARCHAR(4000),   -- size to your largest serialised payload
    PAYLOAD_CLASS  VARCHAR(256),
    PRIMARY KEY (FAILOVER_NAME, FAILOVER_KEY)
);

The PAYLOAD column stores JSON. Adjust its size to accommodate your largest serialised payload. For very large payloads, use CLOB / TEXT instead of VARCHAR.

Supported Databases

Database Upsert dialect
H2 MERGE INTO
PostgreSQL INSERT ... ON CONFLICT DO UPDATE
MySQL / MariaDB INSERT ... ON DUPLICATE KEY UPDATE
Oracle MERGE INTO ... USING DUAL
SQL Server MERGE INTO ... USING (VALUES ...) AS src

Dialect detection is automatic via DatabaseResolver. See Database Resolver How-to for custom configurations.

Async writes reduce latency

With failover.store.async=true (default), write operations run on a virtual-thread executor so they never block the request thread.


Custom

Implement FailoverStore<T> and register it as a Spring @Bean. Auto-configuration detects it via @ConditionalOnMissingBean:

RedisFailoverStore.java
@Component
public class RedisFailoverStore<T> implements FailoverStore<T> {

    @Override
    public void store(ReferentialPayload<T> payload) {
        // write to Redis
    }

    @Override
    public Optional<ReferentialPayload<T>> find(String name, String key) {
        // read from Redis — must return a defensive copy
        return Optional.ofNullable(/* ... */);
    }

    @Override
    public void delete(ReferentialPayload<T> payload) {
        // delete from Redis
    }

    @Override
    public void cleanByExpiry(Instant expiry) {
        // remove all entries where expireOn < expiry
    }
}

Defensive copy in find()

find() must return a copy of the stored entry, not a live reference. Callers mutate upToDate and asOf on the returned object. See ADR 10 for the rationale.


Next Steps