How It Works¶
Failover sits between your Spring bean and its upstream dependency. On success it saves the result; on failure it serves the last saved result — transparently, with no changes to calling code.
Store / Recover Lifecycle¶
sequenceDiagram
participant C as Caller
participant A as FailoverAspect
participant H as FailoverHandler
participant K as KeyGenerator
participant E as ExpiryPolicy
participant S as FailoverStore
participant U as Upstream
C->>A: invoke @Failover method(args)
A->>U: proceed — call upstream
alt Upstream succeeds
U-->>A: result
A->>K: key(failover, args)
K-->>A: storeKey (UUID)
A->>E: computeExpiry(failover)
E-->>A: expireOn = now + TTL
A->>S: store(name, storeKey, result, expireOn)
A-->>C: result (upToDate=true)
else Upstream throws
U-->>A: exception
A->>K: key(failover, args)
K-->>A: lookupKey (same UUID)
A->>S: find(name, lookupKey)
alt Entry found and not expired
S-->>A: ReferentialPayload (defensive copy)
A-->>C: payload (upToDate=false, asOf=storedAt)
else Entry missing or expired
S-->>A: empty / delete expired
A-->>C: null or rethrow (per ExceptionPolicy)
end
end Entry Lifecycle States¶
stateDiagram-v2
direction LR
[*] --> Live : first successful upstream call
Live --> Live : subsequent success — TTL refreshed
Live --> Stale : upstream fails, entry within TTL
Stale --> Live : upstream recovers
Stale --> Expired : TTL window passes
Expired --> [*] : entry deleted on next access or cleanup Callers receive upToDate=true in Live state and upToDate=false in Stale state. Expired entries are never served.
Handler Chain¶
Three handlers compose in a decorator chain:
flowchart TD
A[AdvancedFailoverHandler] -->|delegates to| S[ScatterGatherFailoverHandler]
S -->|delegates to| D[DefaultFailoverHandler]
D -->|reads/writes| FS[(FailoverStore)]
A -. "publishes metrics\nhandles RecoveredPayloadHandler" .-> A
S -. "splits / merges\nper-entity slices" .-> S
D -. "key derivation\nexpiry check\npayload enrichment" .-> D | Layer | Class | Responsibility |
|---|---|---|
| Outermost | AdvancedFailoverHandler | Publishes Micrometer metrics; invokes RecoveredPayloadHandler on null result |
| Middle | ScatterGatherFailoverHandler | Scatter/gather: splits composite payload into per-entity slices; parallel dispatch via virtual threads |
| Innermost | DefaultFailoverHandler | Core logic: key derivation, expiry compute/check, store/find/enrich |
Store Assembly Chain¶
flowchart LR
A[AsyncFailoverStore] --> M[MultiTenantFailoverStore]
M --> B[(base store)]
B --> I[InMemoryFailoverStore]
B --> C[CaffeineFailoverStore]
B --> J[JdbcFailoverStore]
AsyncFailoverStore offloads writes to a virtual-thread executor (active when failover.store.async=true). MultiTenantFailoverStore routes each operation to the correct tenant's base store (active when failover.store.multitenant.enabled=true). Both are transparent decorators.
Key Components¶
FailoverAspect¶
FailoverAspect is a Spring AOP @Around advice that intercepts every method annotated with @Failover. It calls the upstream method and routes the outcome to the handler chain:
- Success path →
FailoverHandler.store(failover, args, result) - Exception path →
FailoverHandler.recover(failover, args, clazz, throwable)
The aspect is activated on any Spring-proxied bean regardless of type (Feign client, @Service, @Component, @Repository).
DefaultFailoverHandler¶
Core store/recover logic:
store— generates the key viaKeyGenerator, computesexpireOnviaExpiryPolicy, enriches the payload viaPayloadEnricher, then callsFailoverStore.store.recover— looks up the entry, checksExpiryPolicy.isExpired, enriches on recovery, deletes expired entries.clean— callsFailoverStore.cleanByExpiry(now)to purge all expired entries.
ReferentialPayload¶
The envelope that wraps every stored entry:
| Field | Type | Description |
|---|---|---|
name | String | Effective name (domain or name from annotation) |
key | String | UUID-derived store key from method args |
upToDate | boolean | true when stored from live upstream result |
asOf | Instant | When this payload was stored |
expireOn | Instant | When the entry expires |
payload | T | The actual upstream response |
Defensive copy contract
FailoverStore.find() must return a defensive copy of the stored entry. Callers mutate upToDate and asOf on the returned object without affecting what is persisted.
Referential and ReferentialAware¶
Two ways to expose failover metadata in your domain type:
Referential— abstract class; addsupToDate,asOf,metadatafields via inheritance.ReferentialAware— interface; implement it when inheritance is not possible.
PayloadEnricher.enrichOnRecover sets upToDate=false and asOf on the recovered payload using whichever contract is present.
Next Steps¶
- Expiry Policies — configuring TTL
- Key Generation — how store keys are derived
- Scatter / Gather — per-entity storage for collections