anycache is a Go cache-aside helper that wraps expensive reads with a consistent API across storage backends.
It is built for teams that want to add caching quickly without reimplementing stampede protection, refresh-on-near-expiry behavior, and cache lifecycle wiring in every service.
- Reduce repeated backend work with lazy, on-demand caching.
- Deduplicate concurrent misses for the same key.
- Keep hot keys fresh with optional warm-up before TTL expiry.
- Switch storage backends (Redis, in-memory, layered, and more) without changing calling code.
- Go services that use cache-aside patterns around DB/API calls.
- Teams that want a small, explicit caching abstraction instead of custom one-off wrappers.
go get github.com/ksysoev/anycachepackage main
import (
"context"
"fmt"
"time"
"github.com/ksysoev/anycache"
redisstorage "github.com/ksysoev/anycache/storage/redis"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer func() { _ = rdb.Close() }()
cache := anycache.New(redisstorage.New(rdb))
defer func() { _ = cache.Close() }()
data, err := cache.Cache(ctx, "user:42", 5*time.Minute, func(context.Context) ([]byte, error) {
return []byte("cached value"), nil
})
if err != nil {
panic(err)
}
fmt.Println(string(data))
}Cache(ctx, key, ttl, generator, opts...) ([]byte, error)for raw[]byteCacheS(ctx, key, ttl, generator, opts...) (string, error)for string valuesCacheStruct(ctx, key, ttl, generator, result, opts...) errorfor JSON-serialized structsInvalidate(ctx, key) errorto remove a keyClose() errorto stop background work gracefully
// CacheS
name, err := cache.CacheS(ctx, "user:name", time.Minute, func(context.Context) (string, error) {
return "alice", nil
})
if err != nil {
panic(err)
}
_ = name
// CacheStruct
type Profile struct {
ID int `json:"id"`
Name string `json:"name"`
}
var p Profile
if err := cache.CacheStruct(ctx, "user:profile", 5*time.Minute, func(context.Context) (any, error) {
return Profile{ID: 42, Name: "Alice"}, nil
}, &p); err != nil {
panic(err)
}
// Invalidate
if err := cache.Invalidate(ctx, "user:profile"); err != nil {
panic(err)
}WithTTLRandomization(percent)— spread expirations to reduce stampedes.WithKeyPrefix(prefix)— namespace keys.WithBaseContext(ctx)— set base context for internal/background work.WithMetricHook(func(key string, op anycache.State, latency time.Duration))— default per-request metric hook.WithCodec(codec)— override the default JSON codec forCacheStruct. AnyCache provides codecs for JSON, MessagePack, BSON, and Gob formats. Seecodec/*for details.
WithWarmUpTTL(d)— if remaining TTL is belowd, serve current value and refresh in background.WithMetric(hook)— override metric hook for one call.WithTimeout(d)— timeout for internal storage + generation work.
Metric states: hit, miss, warm_up, error.
- Singleflight dedupe scope: concurrent requests for the same key are deduplicated within a single
anycache.Cacheinstance. - Warm-up behavior (
WithWarmUpTTL): when a key exists and its remaining TTL is> 0and<= warmUpTTL, anycache returns the current cached value immediately and schedules a background refresh. - Warm-up lock semantics: only one warm-up refresh per key is started at a time; concurrent requests do not start duplicate warm-up goroutines.
- Timeout and base context (
WithTimeout+WithBaseContext): internal storage and generator work runs on the cache base context (default orWithBaseContext). WithWithTimeout, a timeout is applied to that base context for internal work. - Caller cancellation expectations: because internal work uses the cache base context, caller context values/cancellation are not directly propagated into internal storage/generator execution.
- Lifecycle (
Close): callClose()during shutdown to cancel background work and wait for in-flight warm-up goroutines to finish.
Use anycache when you want:
- A consistent cache-aside API for expensive reads in Go services.
- Built-in same-key deduplication to reduce thundering-herd/stampede pressure.
- Optional warm-up refresh behavior without writing custom background orchestration.
- Flexibility to move between in-memory, Redis, layered, or other supported backends.
anycache may be a poor fit when:
- You need backend-specific features directly (for example advanced Redis primitives) as part of core logic.
- Your use case is very small and a direct one-off cache-aside wrapper is simpler to maintain.
- You require highly custom invalidation/orchestration rules that sit outside this abstraction.
- Direct backend client: maximum control, but you manage dedupe, warm-up, and consistency details yourself.
- Hand-rolled cache-aside wrapper: can work for narrow use cases, but tends to duplicate behavior across services over time.
See GitHub Releases for release notes and version-to-version changes. Tags are available at GitHub Tags.
Backends in this repository:
storage/redisstorage/inmemorystorage/layeredstorage/memcachestorage/badger
- Choose
inmemoryfor fastest process-local caching and when restart data loss is acceptable. - Choose
redisfor shared cache state across instances with standard Redis interoperability. - Choose
memcachewhen you need Memcached infrastructure and accept protobuf-wrapped value format + TTL limits. - Choose
badgerfor single-node persistent local caching without external services. - Choose
layeredwhen you want an L1+L2 strategy (for example in-memory + Redis) and can tolerate best-effort, non-atomic cross-layer behavior.
storage/memcache does not store raw user bytes directly. On every Set, it serializes values as protobuf CachedItem:
value(original payload)expires_at_unix(absolute expiration timestamp)
This wrapper is required so GetWithTTL can reconstruct remaining TTL from stored absolute expiry.
Interoperability impact:
- Non-anycache memcached readers will see protobuf bytes, not the original raw payload.
- Non-anycache writers that store raw bytes will not match this backend's decode path.
TTL behavior implemented by this backend:
ttl > 30 daysis rejected.- Positive TTL that truncates below 1 second is rejected.
ttl <= 0means no expiration.
storage/layered executes operations in layer order and does not provide cross-layer transactions.
GetWithTTL flow:
- Reads each layer from top to bottom.
- If a layer returns
ErrKeyNotExists, it continues to the next layer. - If a layer returns any other error, it fails immediately.
- On a lower-layer hit, it back-populates all upper layers using
Setand the returned TTL. - If any back-population
Setfails,GetWithTTLreturns that error (even though a lower layer had the value).
Write/delete flow:
Setwrites sequentially to each layer and returns on first error.Deldeletes sequentially from each layer and returns on first error.- Because writes/deletes are sequential, earlier layers may already be modified when a later layer fails (partial application).
Consistency expectation:
- Layer alignment is best effort over time.
- Temporary divergence between layers is possible during failures or partial updates.
For runnable onboarding examples, see:
anycache_example_test.goin this repository- pkg.go.dev examples: https://pkg.go.dev/github.com/ksysoev/anycache
ctx := context.Background()
store, err := inmemory.New(10_000)
if err != nil {
panic(err)
}
defer func() { _ = store.Close() }()
cache := anycache.New(store)
defer func() { _ = cache.Close() }()
v, err := cache.CacheS(ctx, "greeting", time.Minute, func(context.Context) (string, error) {
return "hello", nil
})
if err != nil {
panic(err)
}
_ = vctx := context.Background()
l1, err := inmemory.New(5_000)
if err != nil {
panic(err)
}
defer func() { _ = l1.Close() }()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer func() { _ = rdb.Close() }()
l2 := redisstorage.New(rdb)
store, err := layered.New(l1, l2)
if err != nil {
panic(err)
}
cache := anycache.New(store)
defer func() { _ = cache.Close() }()
if _, err := cache.Cache(ctx, "user:42", 5*time.Minute, func(context.Context) ([]byte, error) {
return []byte("value"), nil
}); err != nil {
panic(err)
}When a value is found in a lower layer, storage/layered back-populates upper layers for faster subsequent reads.