Configure @Failover¶
@Failover supports several configuration styles — from a single hardcoded expiry to fully expression-driven, environment-specific values. This guide covers all options with examples.
1. Fixed Expiry (Same Across All Environments)¶
Use expiryDuration and expiryUnit when the expiry value is the same in every environment:
@Failover(
name = "country-by-code",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
Country findByCode(String code);
expiryUnit accepts any java.time.temporal.ChronoUnit constant: MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS.
2. Expression-Based Expiry (Environment-Specific Values)¶
Use expiryDurationExpression and expiryUnitExpression to resolve expiry from application properties, environment variables, or any Spring EL expression. When either expression is set, it takes priority over the corresponding fixed attribute.
Property / YAML file¶
@Failover(
name = "country-by-code",
expiryDurationExpression = "${failover.country.expiry-duration:24}",
expiryUnitExpression = "${failover.country.expiry-unit:HOURS}"
)
Country findByCode(String code);
Each profile overrides the expiry independently. The default values (24 / HOURS) apply when the property is absent.
Environment variable¶
@Failover(
name = "country-by-code",
expiryDurationExpression = "${FAILOVER_COUNTRY_EXPIRY_DURATION:24}",
expiryUnitExpression = "${FAILOVER_COUNTRY_EXPIRY_UNIT:HOURS}"
)
Country findByCode(String code);
Set environment variables for production containers:
Spring EL (computed value)¶
@Failover(
name = "country-by-code",
expiryDurationExpression = "#{@failoverProperties.countryExpiryDuration}",
expiryUnitExpression = "#{@failoverProperties.countryExpiryUnit}"
)
Country findByCode(String code);
Any valid Spring EL expression is accepted: bean property access, arithmetic, conditionals.
3. Custom Key Generator¶
By default, the key is derived from the method arguments (joined and hashed to a UUID). Override it with a named bean:
@Component("countryKeyGen")
public class CountryKeyGenerator implements KeyGenerator {
@Override
public String key(Failover failover, List<Object> args) {
// normalise to uppercase for case-insensitive lookup
return ((String) args.get(0)).toUpperCase(Locale.ROOT);
}
}
@Failover(
name = "country-by-code",
keyGenerator = "countryKeyGen",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
Country findByCode(String code);
4. Custom Expiry Policy¶
Override how expiry is calculated for a specific failover:
@Component("countryExpiryPolicy")
public class CountryExpiryPolicy implements ExpiryPolicy<Country> {
@Override
public Instant computeExpiry(Failover failover) {
// expires at midnight UTC
return LocalDate.now(ZoneOffset.UTC).plusDays(1)
.atStartOfDay(ZoneOffset.UTC).toInstant();
}
@Override
public boolean isExpired(Failover failover, ReferentialPayload<Country> payload) {
return payload.getExpireOn().isBefore(Instant.now());
}
}
@Failover(
name = "country-by-code",
expiryPolicy = "countryExpiryPolicy",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
Country findByCode(String code);
When expiryPolicy is set, computeExpiry on the custom policy is called — expiryDuration / expiryUnit are only used by isExpired if the custom policy delegates to the default expiry check.
5. Domain Grouping¶
Share store entries between a single-entity endpoint and a scatter/gather list endpoint:
// Single-entity: stores under domain "country", key derived from "FR"
@Failover(
name = "country-by-code",
domain = "country",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
Country findByCode(String code);
// Batch: scatter-stores each entity individually under domain "country"
@Failover(
name = "countries-by-codes",
domain = "country",
payloadSplitter = "countrySplitter",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
List<Country> findByCodes(String csvCodes);
A successful findByCodes("FR,DE") stores FR and DE individually. On failure, findByCode("FR") recovers FR from the same store partition without a separate findByCodes call ever having succeeded.
Warning
All @Failover annotations in the same domain must use the same expiry configuration. Mismatched expiry causes the last writer to overwrite the stored expiry timestamp. A startup WARN is logged when mismatched expiry is detected.
6. PayloadSplitter + recoverAll Combinations¶
6a. Batch by IDs (standard scatter/gather)¶
@Failover(
name = "countries-by-ids",
domain = "country",
payloadSplitter = "countrySplitter",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
List<Country> findByIds(String csvIds);
countrySplitter.splitOnRecover reads args.get(0) (the CSV), splits on ,, returns one context per ID. Each context drives one delegateR.recover() call. No recoverAll needed.
6b. findAll() — No Args¶
@Failover(
name = "all-countries",
domain = "country",
payloadSplitter = "countryAllSplitter",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS
)
List<Country> findAll();
Empty args automatically trigger the recover-all path. countryAllSplitter.splitOnRecover returns one placeholder context. delegateR.recoverAll fetches all slices under domain = "country".
6c. Filter Args — Non-ID Arguments¶
@Failover(
name = "countries-by-region",
domain = "country",
payloadSplitter = "countryAllSplitter",
expiryDuration = 24,
expiryUnit = ChronoUnit.HOURS,
recoverAll = true
)
List<Country> findByRegion(String region);
region is a filter, not an entity ID. Without recoverAll = true, scatter would try to split "EU" into entity keys (wrong). With recoverAll = true, the recover-all path is forced regardless of non-empty args. countryAllSplitter.splitOnRecover ignores args and returns one placeholder.
Summary¶
| Pattern | Args type | recoverAll | Splitter splitOnRecover |
|---|---|---|---|
| Batch by IDs | Entity IDs (CSV) | false (default) | Split args into per-entity contexts |
| findAll() | None | Not needed | Return 1 placeholder context |
| Filter-only args | Non-ID filters | true | Return 1 placeholder context (ignore args) |
Full Example — All Options Together¶
@Failover(
name = "countries-by-codes",
domain = "country",
expiryDurationExpression = "${failover.country.expiry-duration:24}",
expiryUnitExpression = "${failover.country.expiry-unit:HOURS}",
keyGenerator = "countryKeyGen",
expiryPolicy = "countryExpiryPolicy",
payloadSplitter = "countrySplitter"
)
List<Country> findByCodes(String csvCodes);
Resolution order:
expiryDurationExpression(non-blank) → overridesexpiryDurationexpiryUnitExpression(non-blank) → overridesexpiryUnitexpiryPolicy(non-blank) → overrides default expiry policy (butexpiryDuration/expiryUnitstill supply defaults when the policy delegates to them)keyGenerator(non-blank) → overrides default key generatorpayloadSplitter(non-blank) → enables scatter/gather mode
Next Steps¶
- Annotation Reference — complete attribute table and per-attribute detail
- Scatter / Gather Concepts — how
payloadSplitter+recoverAllwork at runtime - Payload Splitter How-to — step-by-step splitter implementation
- Domain Grouping — sharing entries across failovers