Cache providers
Redis, in-memory, custom — all behind one canonical cache contract with singleflight fill, AEAD wrapping, and four-tier invalidation.
Vadyl ships a foundational caching plane that sits behind every entity read after access enforcement. The cache is post-enforcement (anti-pattern #60), publication-aware, scope-aware, and invalidated only after committed writes — never serves actor-variant data across actors, never serves stale data after a commit.
Bindings
Cache choice is project state — bound through CacheProviderBinding:
bindings: {
cache: {
type: "redis",
configRef: "config:Vadyl:Redis:PrimaryConnection",
isDefault: true,
},
},configRef is a resolvable reference (config:or env:); raw connection strings are never persisted. Bindings inherit through the project hierarchy — closer ancestors win.
Built-in providers
- Redis — production-grade, distributed, supports singleflight via
SETNXleases, AEAD-wrapped payloads. - InMemory — process-local bounded LRU. Marked
IsLocalOnly=true; bootstrap never selects it implicitly. Dev / test / explicit opt-in only.
Custom providers can be authored via the capability-surface system (built-in native, declarative bundle, or Wasm component). All flow through the same router and registry.
Singleflight fill
When two concurrent requests miss the cache for the same key, Vadyl elects one filler via the provider's lease primitive. Losers poll on capped exponential backoff (25ms→250ms, 5 attempts) and consume the winner's value. If the winner fails to publish within the window, losers fall back to their own fill to avoid starvation.
AEAD payload protection
Entities marked with cacheAllowProtected: true have cached payloads wrapped in CachedPayloadEnvelope via the project's key ring. Wrong AAD, wrong key version, or truncated envelope all fail-closed to CacheMiss — the original read path runs and re-fills.
Four-tier invalidation
Physical cache keys compose four generation tiers — platform, global, project, entity. Bumping any tier invalidates everything below. This gives you O(1) namespace invalidation without scanning keys:
- platform —
InvalidateAllAsyncbumps the platform-wide generation; affects every project. - global — per-provider global generation (optimization tier).
- project — bumped by every committed write into the project.
- entity — bumped by writes to a specific entity.
Distributed coherence
Cache invalidations broadcast via the canonical ICacheInvalidationBus (the same bus 13 other caches subscribe to). Peer instances see invalidations within one bus roundtrip — TTL is fallback only, never the primary coherence mechanism. Dormant providers catch up via durable per-project sequence counters when they re-materialize.
Inspecting cache decisions
vadyl explain read --entity Order --filter "id=order_123"
# Output includes:
# cache.decision: hit | miss | bypass | refused
# cache.key: <physical key with all four generation tiers>
# cache.provider: <provider alias>
# cache.reason: stable reason code from ReadPlanReasonCodes