Skip to content
Getting Started

Quickstart

Quickstart

Build a failover-enabled service in 5 minutes — one dependency, one annotation, and your service is protected.


1. Add the Dependency

pom.xml
<dependency>
    <groupId>com.societegenerale.failover</groupId>
    <artifactId>failover-spring-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>
build.gradle.kts
implementation("com.societegenerale.failover:failover-spring-boot-starter:3.0.0")

2. Configure the Store

application.yml
# No additional config needed — in-memory is the default.
# Not suitable for production: data is lost on restart.
failover:
  enabled: true
application.yml
failover:
  store:
    type: caffeine
application.yml
failover:
  store:
    type: jdbc
    jdbc:
      table-prefix: DEMO_
create_table.sql
CREATE TABLE DEMO_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),
    PAYLOAD_CLASS  VARCHAR(256),
    PRIMARY KEY (FAILOVER_NAME, FAILOVER_KEY)
);

-- Required: keeps the expiry-cleanup DELETE (`WHERE EXPIRE_ON < ?`) off a full table scan.
CREATE INDEX IDX_DEMO_FAILOVER_STORE_EXPIRE_ON ON DEMO_FAILOVER_STORE (EXPIRE_ON);

3. Define Your Domain Type

Your return type must extend Referential or implement ReferentialAware to carry failover metadata (upToDate, asOf) back to callers.

Country.java
@Data
@EqualsAndHashCode(callSuper = false)
public class Country extends Referential {
    private String code;
    private String name;
    private String currency;
}

Referential adds three fields:

Field Type Description
upToDate boolean true when fetched live; false when recovered from store
asOf Instant When the payload was originally stored
metadata Metadata Optional carrier for additional context
Country.java
@Data
public class Country implements ReferentialAware {
    private String code;
    private String name;
    private boolean upToDate;
    private Instant asOf;
    private Metadata metadata;

    @Override
    public void setUpToDate(boolean upToDate) { 
        this.upToDate = upToDate; 
    }

    @Override
    public void setAsOf(Instant asOf) { 
        this.asOf = asOf; 
    }

    @Override
    public void setMetadata(Metadata metadata) { 
        this.metadata = metadata; 
    }
}

Use ReferentialAware when you cannot extend Referential (e.g. the class already has a superclass).


4. Annotate Your Method

Place @Failover on a method of a Spring-managed bean. The annotation works on any bean type: @Service, @Component, @FeignClient, etc.

CountryClient.java
@FeignClient(name = "country-service", url = "${country.service.url}")
public interface CountryClient {

    @Failover(
        name = "country-by-code",
        expiryDuration = 24,
        expiryUnit = ChronoUnit.HOURS
    )
    Country findByCode(@RequestParam String code);

    @Failover(
        name = "all-countries",
        expiryDuration = 1,
        expiryUnit = ChronoUnit.DAYS
    )
    List<Country> findAll();
}

Annotate the implementation, not the interface

Spring AOP uses CGLIB proxies on concrete classes. If your @Failover is on an interface method (like a Feign client), the framework still intercepts it — but if you use CGLIB proxies directly, the annotation must be on the concrete class method.

name vs domain — don't confuse them

  • name (required) identifies this failover point. It is the metric label and the default store grouping. Keep it unique per method — two methods sharing a name (without an explicit domain) will share store entries and clash.
  • domain (optional) deliberately groups several @Failover methods so they share store entries — e.g. a single-fetch and a batch-fetch over the same referential. Methods in one domain must agree on expiry; a mismatch is warned at startup.

Rule of thumb: set only name until you actually need two methods to read each other's stored data — then give them the same domain. See Domain Grouping.


5. What You Get

On every successful upstream call, Failover stores the result with the configured TTL:

INFO  FailoverHandler: Storing information on 'country-by-code' for failover.
      ReferentialPayload: {name=country-by-code, key=<UUID>, upToDate=true, asOf=..., expireOn=...}

On upstream failure, the last stored result is served automatically:

INFO  FailoverHandler: Recovering information on 'country-by-code' from failover store
      due to exception: Connection refused
INFO  FailoverHandler: Successfully recovered the information on 'country-by-code'.
      ReferentialPayload: {upToDate=false, asOf=2024-01-15T10:30:00Z}

The returned object has upToDate=false and asOf set to the original store timestamp:

Country country = countryClient.findByCode("FR");
System.out.println(country.isUpToDate());  // false (recovered from store)
System.out.println(country.getAsOf());     // 2024-01-15T10:30:00Z

6. Scatter / Gather (Optional)

For collection-returning methods, use a PayloadSplitter to store each entity individually — enabling partial recovery when only some entries are available.

CountryClient.java
@Failover(
    name = "countries-by-codes",
    domain = "country",                      // shares store with country-by-code
    payloadSplitter = "countrySplitter",
    expiryDuration = 24,
    expiryUnit = ChronoUnit.HOURS
)
List<Country> findByCodes(@RequestParam String codes);  // codes = "FR,DE,US"

See Scatter / Gather for the full implementation guide.


Next Steps