Quickstart¶
Build a failover-enabled service in 5 minutes — one dependency, one annotation, and your service is protected.
1. Add the Dependency¶
2. Configure the Store¶
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.
@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 |
@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.
@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 aname(without an explicitdomain) will share store entries and clash.domain(optional) deliberately groups several@Failovermethods so they share store entries — e.g. a single-fetch and a batch-fetch over the same referential. Methods in onedomainmust 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.
@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¶
- How It Works — full lifecycle explanation
- Properties Reference — all configuration options
- Store Types — choose the right backing store