Status: pre-1.0 (0.x development line). This document is the canonical answer to "what is stable and what isn't?" during the 0.x cycle. It supersedes the pre-1.0 caveat previously buried in
CHANGELOG.md.
Adopting teams can pin to a 0.x version with confidence that the surface described in §1 and §3 will not break in patch releases.
The following are stable and will not change in any 0.x release without
a documented migration path (CHANGELOG.md):
| Surface | Stable form | Notes |
|---|---|---|
| Enhancement annotation signatures | @RedisCacheable, @RedisCachePut, @RedisCacheEvict, @RedisCaching |
Attribute names, types, and documented semantics. Compatibility-only attributes are explicit below; adding new attributes is non-breaking. |
| Configuration property keys | resi-cache.* namespace under application.yml / application.properties |
Property names and types. Adding new properties is non-breaking. |
| Wire format | {version, payload} envelope used by SecureJacksonRedisSerializer |
Envelope is the serialization contract — kept, not loosened. |
| Extension SPI | CacheHandler, ChainObserver, BloomIFilter, LockManager, LockManager.LockHandle, HandlerPriority |
Implementations must satisfy the documented failure, lifecycle, and thread-safety contracts. |
| SPI transitive contract types | CacheContext, CachePolicyView, HandlerResult, CacheResult, CacheOperation, FlowControl, ChainContinuation, HandlerOrder, and decision records used by handler signatures |
These signature/value types and the HandlerOrder numeric ordering contract are part of the supported SPI surface; unrelated fields and implementation classes remain unstable. |
| Native writer statistics | Spring Data Redis GET/GET hit/GET miss/PUT/DELETE counters are emitted at the writer boundary; CLEAN carries its exact deleted-key count through stable CacheResult |
withStatisticsCollector rebinds all statistics; lock-wait duration remains unreported (getLockWaitDuration() is zero) until an internal observation path can be added without expanding CacheContext |
The following members remain part of the stable source/binary surface, but are not active runtime controls on the current blocking Spring Cache path:
@RedisCacheEvict.unlessis retained for compatibility but is not evaluated. Spring'sCacheEvictOperationhas no after-invocationunlessslot; useconditionfor supported eviction gating.@RedisCacheable.typeand@RedisCachePut.typeare retained as declaration metadata. They do not coerce or validate the runtime value; the returned value and serializer determine its actual type.
Activating either behavior requires a separate operation-semantics decision and regression contract; these members must not be removed as dead code in 0.x.
If you pin to a specific 0.x.y version, these are guaranteed within the 0.x line.
| Area | What may change | Example |
|---|---|---|
| Internal implementation | Source-level details inside chain/, protection/, cache/ |
Handler ordering is fixed by HandlerOrder enum (gap = 100), but inner algorithm of a specific handler is not contractual |
| Default values of properties | Defaults may be tuned between minor versions | resi-cache.default-ttl default may shift toward a better baseline |
| Unstable package layout | Contents of internal sub-packages and unstable implementation types under io.github.davidhlp.spring.cache.redis.* |
Stable annotations, configuration keys, wire format, and SPI signature types listed in §1 are excluded. |
| Observability metric names and tags | Pre-1.0 metric namespace is NOT contractual | A bloomsift.* → resicache.handler.* rename is allowed pre-1.0 (with |
| Diagnostic warnings and logs | Message text, log levels for startup probes | "whitelist auto-derived from host app root package" WARN may rephrase |
| Behavior defaults (e.g. protection preset) | When explicitly opted into a new default via |
resi-cache.protection.preset=NONE (v0.0.2) → =STANDARD (v0.0.3) is allowed if flagged breaking |
| Internal implementation types | MethodMetadataResolver, MethodSnapshot, ScopedActivation, RefreshCancellation, LoaderOrchestrator, LoadOutcome, and ThreadPoolEarlyExpirationExecutor |
Package-private collaborators under the internal cache module; not importable extension contracts. |
If you depend on items in this section, pin to an exact patch version
(0.x.y) and review CHANGELOG.md entries on upgrade.
| Type | Replacement | Deprecation/removal | Impact |
|---|---|---|---|
MethodMetadataResolver / MethodSnapshot |
internal resolver lifecycle via auto-configuration | internalized in the Phase 4 cache module | source/binary break for custom resolver implementations |
LoaderOrchestrator / LoadOutcome (and the former DefaultLoadFn) |
RedisProCache.get(key, loader) |
internalized in the Phase 4 cache module | callers must use the cache API, not loader callbacks |
CacheContext / HandlerResult / decision records |
documented SPI value surface for handler signatures; implementation-only members may evolve | no removal while CacheHandler/ChainObserver remain supported |
extensions use documented fields and flow values |
ThreadPoolEarlyExpirationExecutor |
documented stable SPI only; concrete executor remains internal | internalized in the Phase 4 cache module | custom code uses stable interfaces, not implementation classes |
Removal is not activated solely from local source evidence. A published artifact, adopter usage, or external implementation supersedes this default plan and changes the migration decision.
@RedisCacheable,@RedisCachePut,@RedisCacheEvict,@RedisCachingattribute names and typesresi-cache.*property keys- The
{version, payload}envelope wire format
These are the absolute minimum a downstream user needs to upgrade between 0.x patch versions without code changes.
Implementing the documented SPI means agreeing to the following protocol. The engine enforces the machine-checkable parts; the rest is the contract a custom implementation must satisfy.
- Non-null result: handling a node MUST return a non-null
HandlerResult. The engine callshandle(context, next)and rejectsnullwithIllegalStateException("CacheHandler returned null HandlerResult: <class>")— never an opaque NPE. A handler whose work requires the continuation (see item 6) may reject a barehandle(context)call withIllegalStateExceptionrather than silently run a partial chain. - FlowControl semantics:
CONTINUEadvances to the next handler (anullresult field at chain end materializes tosuccess());TERMINATEends the chain and returns the carried result;SKIP_ALLends the chain, returns the carried result, and sets the engine-onlyskipRemainingmarker so no further handler runs. - Post-process: only handlers whose
requiresPostProcess(context)returnstruegetafterChainExecution(context, result)after the main chain completes. Exceptions thrown there are caught and logged by the engine; they never alter the main-chain result. - Ordering:
@HandlerPriority(HandlerOrder.X)is the single source of truth (gap = 100). Unannotated handlers sort last. - Thread safety: one handler instance is shared across concurrent
executions; keep per-call state out of fields (use
CacheContext). - Nested advancement (optional): the engine calls
handle(context, ChainContinuation next), whose default ignoresnextand delegates tohandle(context)— existing implementations are unaffected. A handler that overrides it may run the remainder of the chain inside its own critical section (this is howsync=trueruns the chain inside the distributed lock). The handle is valid only for that call, may be advanced at most once (a second call throwsIllegalStateException), stays on the calling thread, and carries per-node observation only — around-chain observation and post-processing remain owned by the outer execution. After advancing, the handler MUST end its node withTERMINATE: returningCONTINUEwould make the engine run every successor a second time and is rejected withIllegalStateException. - Non-null decision:
HandlerResultconstruction MUST provide a non-nullFlowControl; its public canonical constructor rejectsnullwith aNullPointerExceptionexplaining that the SPI protocol requires a decision. If a malformed result reaches the engine, it rejects it with anIllegalStateExceptionnaming the offending handler before dispatching the decision.
- Hook order per chain execution:
onChainStart→ per node [onNodeStart→handler.handle(context, next)→afterNode→onNodeEnd] →onChainEnd. Multiple observers run in registration (@Order) order for every hook. - Scope tokens: each
on*Startreturns a per-call token the engine pairs back to the same observer'son*Endin afinallyblock (on handler exceptiononNodeEndreceives anullresult — recover the token, do not fabricate decisions). Tokens carry per-call state; observers must be thread-safe and stateless between calls. The engine pairs by observer registration index, so the token anon*Endhook receives is always the reference returned by that observer's matchingon*Start: an observer may cast its own token to its private token type without a runtime type check. - Exception isolation: observer hook failures are caught and logged by the engine; they never change chain control flow.
beforeNode removed (0.x): the SPI hook
ChainObserver.beforeNode(CacheHandler, CacheContext) no longer exists; it had
zero production implementers and no other hook was renamed, retyped or
reordered. Migration from an implementation that overrode it:
- node pre-execution work (DEBUG log / counter increments / start markers) →
move the body to
onNodeStart, returningnullwhen the observer keeps no per-call state; - work that needs the evaluated result → move the body to
afterNode; - per-call state that a matching hook must recover → return it from
onNodeStartand read it back inonNodeEnd(the documented token pairing above); the engine still callsonNodeEndfrom afinallyblock.
CacheContextexposes a read-onlyInputView(operation, cacheName, redisKey, actualKey, valueBytes, deserializedValue, ttl, policy) plus the typed decisions (TtlDecision,NullDecision,PrefetchDecision,keyPattern) that named producer handlers write andActualCacheHandlerreads. Custom handlers SHOULD read only; writing decisions is reserved for the documented producer/consumer pairs.markSkipRemaining()is engine-only state materialized fromSKIP_ALL; custom handlers must not call it.byte[]values cross the SPI by defensive copy (CacheResult.resultBytes()); do not mutate arrays handed to you and do not rely on retaining them.
The machine gate (public-surface-nested.txt +
PublicSurfaceContractTest) pins this list. Classification:
| Nested public type | Class | Notes |
|---|---|---|
CacheResult.Outcome / FailureKind |
user | stable value semantics |
CacheContext.InputView |
user | read-only input view for handlers/tests |
CachePolicyView.Source |
implementation | adapter interface implemented by internal operation models; public only so internals can implement it — do not use from host code |
RedisProCacheProperties.* (9 nested classes) |
user | configuration binding surface |
CachingEnablementValidation.CachingEnabledValidator |
operator | health/startup probe |
RedisDeploymentValidator$RedisDeploymentChecks |
implementation | internal validation payload |
LockManager.LockHandle |
extension | stable lock handle (§1) |
SerializationException.EnvelopeCodec |
operator | envelope helpers for migration tooling |
SerializationMigrationCli$SerializationMigrationRunner, SerializationMigrationProperties$LegacySerializer |
operator | migration CLI surface |
cache.*$* (Builder/Scope/Strategy/Outcome records etc.) |
implementation | outer classes are package-private internals — not reachable from host code |
Removing or renaming an extension/user entry is a implementation entries may be internalized without a major bump.
Graduation to 1.0 is a pre-1.0 milestone not yet reached. The markers below
describe what 1.0 will mean and are aspirational until the 1.0.0 tag is cut:
- Public surface stability — §1 + §3 have held across at least one release cycle without breaking changes.
- Production-grade ops surface — Maven Central publish under
io.github.davidhlp, CycloneDX SBOM per release, OWASP dependency-check gate at HIGH/CRITICAL. - Adoption signal — at least one production adopter listed in
ADOPTERS.md(created when the first adopter lands). - Bus factor — a named successor or a documented succession plan
(see
CONTRIBUTING.md→ Maintainers & bus factor).
When 1.0 ships, only the caller-observable contracts — §1 plus the §3
minimum (annotation attributes, resi-cache.* keys, wire envelope,
documented SPI behavior, typed failure semantics) — become locked;
changing one is a new major version. Items in §2 (internals, defaults,
metric names, log wording) stay minor-version evolvable in 1.x, flagged
with a COMPATIBILITY.md.
- Pin to exact
0.x.yversions if you depend on §2 behavior. - Pin to
0.x(minor-flexible) if you depend only on §1 + §3. - At
1.x, SemVer major bumps protect only the §1 + §3 caller-observable contracts above. Metric names, log messages, and behavior defaults remain minor-version evolvable (§2) unless a bounded metric/label contract with migration notes is published.
CHANGELOG.md— per-version changelog including⚠️ BREAKING markers.
Current architecture ownership and design constraints live in
docs/ARCHITECTURE.md; Git history records ordinary implementation history and commit-level details.