Redis cache provider
Redis cache provider implementing cache.Provider against a single-node Redis server, with tag-based invalidation, optional RediSearch indexing, and a shared client connection across namespaces.
Overview
The Redis provider turns Redis into a Piko cache backend. Every replica that points at the same Redis server shares the state, so a write on one pod is visible to every other pod. That distinguishes it from Otter, the in-process default. The provider tracks tags in Redis sets, so InvalidateByTags("user:42") removes every cache entry carrying that tag. Tag writes use SADD, invalidation reads the union of the tag sets with SUNION, then deletes the matched keys in a pipeline.
Reach for Redis in three cases:
- State needs to span replicas.
- One cache covers multiple services.
- You need RediSearch secondary indexing for cache-side query patterns.
Reach for Otter for in-process speed in single-instance setups. Reach for Redis Cluster when one Redis server runs out of capacity. Reach for Valkey when you need a permissive licence, the BSD-licensed fork of Redis. Reach for a multilevel Otter and Redis combination to put a process-local L1 in front of the network hop. Each of these implements the same cache.Provider port, so switching backends is a one-line provider swap with no change to your cache call sites.
The provider opens a single redis.Client and shares it across all namespaces created through it. Each namespace becomes a key prefix. Compute and atomic operations run optimistic-locking semantics over the network through WATCH/MULTI/EXEC, retrying up to MaxComputeRetries times. You must supply Registry, a cache cannot encode values without it.
Requirements
- A reachable Redis server (single node, see Redis Cluster for sharded deployments).
- Network egress to the configured
Address. - A
cache.EncodingRegistrydescribing how to encode the value types you cache. JSON or Gob encoders cover most cases. - The RediSearch module on the server if you attach a search schema. Vanilla Redis without the module fails the
FT.CREATEstep.
Configuration
import (
"os"
"time"
"piko.sh/piko/wdk/cache"
"piko.sh/piko/wdk/cache/cache_encoder_json"
"piko.sh/piko/wdk/cache/cache_provider_redis"
)
// Build an encoding registry for the cache values. The encoder needs an
// AnyEncoder assertion, because New returns the typed EncoderPort interface.
jsonEncoder := cache_encoder_json.New[any]()
registry := cache.NewEncodingRegistry(jsonEncoder.(cache.AnyEncoder))
provider, err := cache_provider_redis.NewRedisProvider(cache_provider_redis.Config{
Address: "localhost:6379", // required: host:port
Password: os.Getenv("REDIS_PASSWORD"), // empty when the server requires no authentication
DB: 0, // Redis database number
Namespace: "myapp:", // global key prefix
DefaultTTL: 1 * time.Hour, // entry expiry; default 1 hour
OperationTimeout: 2 * time.Second, // standard ops; default 2s
Registry: registry, // required: value encoder registry
})
if err != nil {
return err
}
NewRedisProvider pings the server during construction and fails fast if it is unreachable or the registry is nil. Leave the timeout block out and the provider fills sane defaults from a shared defaults table. DefaultTTL defaults to 1 hour, OperationTimeout to 2 seconds, AtomicOperationTimeout to 5 seconds, BulkOperationTimeout to 10 seconds, FlushTimeout to 30 seconds, SearchTimeout to 5 seconds, and MaxComputeRetries to 10. Each applies only when the field is zero.
Bootstrap
ssr := piko.New(
piko.WithCacheProvider("redis", provider),
piko.WithDefaultCacheProvider("redis"),
)
Creating a cache
After you register the provider, obtain a typed cache from the cache service with cache.CreateNamespace. Pass the provider name, a namespace, and the cache options:
opts := cache.Options[string, User]{MaximumSize: 10000}
userCache, err := cache.CreateNamespace[string, User](ctx, service, "redis", "users", opts)
The provider handles a fixed set of key and value combinations through its erased entry point: string keys with []byte, string, int, or any values, plus int keys with string, int, or any values. To cache a custom domain value type, register a provider factory with cache.RegisterProviderFactory() first. Without it, the namespace call returns an error naming the unsupported type.
RediSearch is opt-in through a search schema on the cache options, not through Config. Build one with cache.NewSearchSchema and set it as the SearchSchema field of the options. The IndexPrefix config field only names the index, and SearchTimeout only bounds the search call. With no schema attached, Search and Query return ErrSearchNotSupported.
See also
Other cache providers:
- Otter, in-process cache for single-instance setups.
- Redis Cluster, sharded Redis for horizontal scale.
- Valkey, BSD-licensed Redis fork.
- Multilevel, Otter L1 in front of a Redis L2.
Cache encoders and transformers:
- JSON encoder, register with the value
Registry. - Gob encoder, Go-native encoding alternative.
- Crypto transformer, encrypt cached values.
- Zstd transformer, compress cached values.
Framework docs:
- How to use the cache, wiring the cache service end-to-end.
- Cache API reference, every type and method on the cache service.
- About caching, design rationale for the cache port.
External:
- Redis commands, authoritative protocol reference.
- RediSearch, secondary indexing module.