cache

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 14 Imported by: 0

README

cache

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

cache is a typed Go cache library with explicit hit, miss, stale, decode, and backend-failure semantics. It provides bounded cache-aside loading, versioned and hashed keys, strict codecs, a bounded memory backend, and native Redis and Valkey adapters. Valkey-backed caches can also publish a refresh only while a compatible distributed lease is still owned.

The semantic API does not expose Redis or Valkey client types. Backends keep their native clients and atomic behavior while applications share one portable contract.

Install

go get github.com/faustbrian/go-cache

Go 1.25 or newer is required.

Quickstart

package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache"
	"github.com/faustbrian/go-cache/backend/memory"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func main() {
	ctx := context.Background()
	backend, err := memory.New(memory.Config{
		MaxEntries: 10_000,
		MaxBytes:   64 << 20,
		Clock:      cache.SystemClock{},
	})
	if err != nil {
		panic(err)
	}

	keys, err := cache.NewKeySpace("accounts", "user", 1,
		cache.StringKeyEncoder{}, 128)
	if err != nil {
		panic(err)
	}
	users, err := cache.New(cache.Config[string, User]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[User]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: 5 * time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1 << 20,
		Load: cache.LoadPolicy{
			MaxConcurrent:    64,
			MaxWaitersPerKey: 256,
			NegativeTTL:      30 * time.Second,
		},
	})
	if err != nil {
		panic(err)
	}
	defer users.Close()

	result, err := users.GetOrLoad(ctx, "user-42",
		func(ctx context.Context, id string) (cache.LoadResult[User], error) {
			// Replace this with a context-aware source lookup.
			return cache.LoadResult[User]{Value: User{ID: id, Name: "Ada"}, Found: true}, nil
		})
	if err != nil {
		panic(err)
	}
	fmt.Println(result.State, result.Value.Name)
}

Always inspect Result.State; a stored zero value can be a Hit. A miss is not an error. Backend, decoding, schema, policy, limit, and loader failures remain errors and can be classified with errors.Is.

For distributed refreshes, acquire a Valkey lease through github.com/faustbrian/go-lease/valkey, derive its opaque guard with Store.Guard, and pass that guard to Cache.SetIfOwned or Cache.SetNegativeIfOwned. The cache record and active owner/token comparison occur in one Valkey script. Protected negative publication uses the configured NegativeTTL. The lease store and cache backend must use the same standalone Valkey deployment.

Choose a policy

  • Use plain cache-aside for data where a source lookup is affordable.
  • Add a short NegativeTTL only when source-level absence is authoritative.
  • Use StaleWhileRevalidate when low latency is more important than returning the refresh error.
  • Use StaleIfError when callers must see refresh failures but may also use the stale value. It returns both the stale Result and the error.
  • Do not enable both stale policies; construction rejects ambiguous precedence.

See policy decisions and failure modes before enabling stale behavior.

Backends and observability

Service lifecycle

cacheservice.New adapts an explicit concrete cache, Redis, or Valkey resource to service.Component. Startup validation and readiness are opt-in callbacks that receive the service context and the concrete resource. Callers add the readiness check to service.Plan only when cache availability is required to accept new work.

Omitting Shutdown keeps the resource shared and guarantees the adapter never closes it. Providing Shutdown explicitly transfers close ownership. The adapter performs no retries, closes a transferred resource once after draining later-declared components, and preserves startup and partial-cleanup failures.

Documentation

Development

make check
make integration
make fuzz
make benchmark

make check enforces formatting, vet, lint, unit tests, meaningful exact coverage, race safety, vulnerability scanning, GO-SAFETY-1, and docs compilation. Integration tests use Testcontainers and require Docker.

Status

The public API is being prepared for v1.0.0. Until that release, minor versions may change APIs. See CHANGELOG.md and the compatibility policy.

License

MIT. See LICENSE.

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package cache provides typed, backend-independent cache semantics.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMiss identifies an explicit cache miss where an error value is needed.
	ErrMiss = errors.New("cache miss")
	// ErrBackend identifies a storage or transport failure.
	ErrBackend = errors.New("cache backend error")
	// ErrDecode identifies malformed serialized data.
	ErrDecode = errors.New("cache decode error")
	// ErrSchemaMismatch identifies an incompatible payload version.
	ErrSchemaMismatch = errors.New("cache schema mismatch")
	// ErrInvalidKey identifies invalid key configuration or encoding.
	ErrInvalidKey = errors.New("invalid cache key")
	// ErrKeyTooLarge identifies a backend key beyond its configured bound.
	ErrKeyTooLarge = errors.New("cache key too large")
	// ErrValueTooLarge identifies a payload beyond its configured bound.
	ErrValueTooLarge = errors.New("cache value too large")
	// ErrInvalidTTL identifies an invalid or already expired deadline.
	ErrInvalidTTL = errors.New("invalid cache TTL")
	// ErrCapacity identifies a record that cannot fit a bounded backend.
	ErrCapacity = errors.New("cache capacity exceeded")
	// ErrClosed identifies use after cache or backend shutdown.
	ErrClosed = errors.New("cache backend closed")
	// ErrLoader identifies a source loader failure.
	ErrLoader = errors.New("cache loader error")
	// ErrLoaderPanic identifies a recovered loader panic.
	ErrLoaderPanic = errors.New("cache loader panic")
	// ErrRecursiveLoad identifies a loader re-entering the same cache.
	ErrRecursiveLoad = errors.New("recursive cache load")
	// ErrWaiterLimit identifies excess callers for one active key flight.
	ErrWaiterLimit = errors.New("cache waiter limit exceeded")
	// ErrInvalidPolicy identifies invalid or contradictory policy options.
	ErrInvalidPolicy = errors.New("invalid cache policy")
	// ErrBatchTooLarge identifies a bulk request beyond its configured bound.
	ErrBatchTooLarge = errors.New("cache batch too large")
	// ErrInvalidRecord identifies malformed portable backend state.
	ErrInvalidRecord = errors.New("invalid cache record")
	// ErrInvalidConfig identifies invalid constructor dependencies or limits.
	ErrInvalidConfig = errors.New("invalid cache configuration")
	// ErrOwnershipLost identifies a protected write rejected by its backend.
	ErrOwnershipLost = errors.New("cache ownership lost")
	// ErrOwnershipUnsupported identifies a backend without atomic ownership validation.
	ErrOwnershipUnsupported = errors.New("cache ownership validation unsupported")
)

Functions

This section is empty.

Types

type Backend

type Backend interface {
	Get(context.Context, string) (Record, bool, error)
	Set(context.Context, string, Record, Condition) (bool, error)
	Delete(context.Context, string) (bool, error)
}

Backend is the atomic storage contract implemented by cache adapters.

type BulkResult

type BulkResult[K, V any] struct {
	Key K
	Result[V]
	Err error
}

BulkResult reports one GetMany result without flattening per-key errors.

type Cache

type Cache[K, V any] struct {
	// contains filtered or unexported fields
}

Cache provides typed cache operations over a backend.

func New

func New[K, V any](config Config[K, V]) (*Cache[K, V], error)

New validates config and constructs a typed cache.

func (*Cache[K, V]) Add

func (c *Cache[K, V]) Add(ctx context.Context, logical K, value V) (bool, error)

Add writes a value only when the key has no live record.

func (*Cache[K, V]) Close

func (c *Cache[K, V]) Close() error

Close cancels active loads, waits for their cleanup, and rejects new work.

func (*Cache[K, V]) Delete

func (c *Cache[K, V]) Delete(ctx context.Context, logical K) (err error)

Delete removes a logical key. Deleting an absent key succeeds.

func (*Cache[K, V]) DeleteMany

func (c *Cache[K, V]) DeleteMany(ctx context.Context, keys []K) ([]MutationResult[K], error)

DeleteMany deletes keys in input order and records per-key failures.

func (*Cache[K, V]) Get

func (c *Cache[K, V]) Get(ctx context.Context, logical K) (result Result[V], err error)

Get returns an explicit hit, miss, or stale result.

func (*Cache[K, V]) GetMany

func (c *Cache[K, V]) GetMany(ctx context.Context, keys []K) ([]BulkResult[K, V], error)

GetMany reads keys in input order and records per-key failures in the result.

func (*Cache[K, V]) GetOrLoad

func (c *Cache[K, V]) GetOrLoad(ctx context.Context, logical K, loader Loader[K, V]) (Result[V], error)

GetOrLoad returns a cached value or coalesces a bounded source load.

Example
package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache"
	"github.com/faustbrian/go-cache/backend/memory"
)

func main() {
	backend, _ := memory.New(memory.Config{
		MaxEntries: 100,
		MaxBytes:   1 << 20,
		Clock:      cache.SystemClock{},
	})
	keys, _ := cache.NewKeySpace("example", "greeting", 1, cache.StringKeyEncoder{}, 128)
	store, _ := cache.New(cache.Config[string, string]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[string]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1024,
	})
	defer func() { _ = store.Close() }()

	result, err := store.GetOrLoad(context.Background(), "hello",
		func(context.Context, string) (cache.LoadResult[string], error) {
			return cache.LoadResult[string]{Value: "world", Found: true}, nil
		})
	fmt.Println(result.State == cache.Hit, result.Value, err)
}
Output:
true world <nil>

func (*Cache[K, V]) Replace

func (c *Cache[K, V]) Replace(ctx context.Context, logical K, value V) (bool, error)

Replace writes a value only when the key has a live record.

func (*Cache[K, V]) Set

func (c *Cache[K, V]) Set(ctx context.Context, logical K, value V) error

Set writes a value without an existence precondition.

func (*Cache[K, V]) SetIfOwned

func (c *Cache[K, V]) SetIfOwned(
	ctx context.Context,
	logical K,
	value V,
	guard OwnershipGuard,
) error

SetIfOwned atomically writes a value only while guard identifies the active backend owner. Ownership loss is reported as ErrOwnershipLost.

func (*Cache[K, V]) SetMany

func (c *Cache[K, V]) SetMany(ctx context.Context, entries []Entry[K, V]) ([]MutationResult[K], error)

SetMany writes entries in input order and records per-key failures.

func (*Cache[K, V]) SetNegativeIfOwned

func (c *Cache[K, V]) SetNegativeIfOwned(
	ctx context.Context,
	logical K,
	guard OwnershipGuard,
) error

SetNegativeIfOwned atomically writes an explicit negative record only while guard identifies the active backend owner. The configured NegativeTTL must be positive. Ownership loss is reported as ErrOwnershipLost.

type Clock

type Clock interface {
	Now() time.Time
}

Clock supplies time for deterministic expiration behavior.

type Codec

type Codec[V any] interface {
	Encode(V) ([]byte, error)
	Decode([]byte) (V, error)
}

Codec serializes and deserializes typed cache values.

type Condition

type Condition uint8

Condition controls the atomic precondition applied by Backend.Set.

const (
	// Unconditional always writes the record.
	Unconditional Condition = iota
	// IfAbsent writes only when the key does not hold a live record.
	IfAbsent
	// IfPresent writes only when the key holds a live record.
	IfPresent
)

type Config

type Config[K, V any] struct {
	Backend  Backend
	Keys     KeySpace[K]
	Codec    Codec[V]
	TTL      TTLPolicy
	Clock    Clock
	MaxValue int
	MaxBatch int
	Load     LoadPolicy
	Jitter   JitterSource
	Observer Observer
}

Config contains all dependencies, limits, and policies for a Cache.

type Entry

type Entry[K, V any] struct {
	Key   K
	Value V
}

Entry pairs a logical key with a value for SetMany.

type Error

type Error struct {
	Kind      ErrorKind
	Operation Operation
	Cause     error
}

Error combines a stable semantic kind with the underlying cause.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() []error

type ErrorKind

type ErrorKind uint8

ErrorKind classifies an operation failure independently of its cause.

const (
	// BackendError identifies storage or transport failures.
	BackendError ErrorKind = iota + 1
	// DecodeError identifies malformed encoded values.
	DecodeError
	// SchemaMismatchError identifies incompatible payload versions.
	SchemaMismatchError
	// InvalidKeyError identifies invalid key configuration or encoding.
	InvalidKeyError
	// LimitError identifies a configured resource-limit violation.
	LimitError
	// PolicyError identifies an invalid or contradictory policy.
	PolicyError
	// LoaderError identifies a source loader failure.
	LoaderError
)

type Event

type Event struct {
	Operation Operation
	Outcome   Outcome
	Duration  time.Duration
	Size      int
}

Event is a redacted semantic observation with no key or value fields.

type JSONCodec

type JSONCodec[V any] struct {
	Version        byte
	MaxEncodedSize int
}

JSONCodec stores strict JSON behind a one-byte schema version.

func (JSONCodec[V]) Decode

func (c JSONCodec[V]) Decode(encoded []byte) (V, error)

Decode validates size and schema version before strict JSON decoding.

func (JSONCodec[V]) Encode

func (c JSONCodec[V]) Encode(value V) ([]byte, error)

Encode serializes a value with its configured schema version.

type JitterSource

type JitterSource interface {
	Duration(time.Duration) time.Duration
}

JitterSource chooses how much time to subtract from a loaded value's TTL.

type KeyEncoder

type KeyEncoder[K any] interface {
	EncodeKey(K) ([]byte, error)
}

KeyEncoder deterministically converts a typed logical key to bytes.

type KeySpace

type KeySpace[K any] struct {
	// contains filtered or unexported fields
}

KeySpace hashes logical keys beneath a namespace, name, and version prefix.

func NewKeySpace

func NewKeySpace[K any](
	namespace string,
	name string,
	version uint32,
	encoder KeyEncoder[K],
	maxKeySize int,
) (KeySpace[K], error)

NewKeySpace validates and constructs an isolated versioned key space.

func (KeySpace[K]) Key

func (s KeySpace[K]) Key(logical K) (string, error)

Key returns a deterministic backend key without exposing logical key bytes.

type LoadPolicy

type LoadPolicy struct {
	MaxConcurrent        int
	MaxWaitersPerKey     int
	NegativeTTL          time.Duration
	StaleWhileRevalidate bool
	StaleIfError         bool
	RefreshJitter        time.Duration
}

LoadPolicy bounds loading and enables optional negative and stale behavior.

type LoadResult

type LoadResult[V any] struct {
	Value V
	Found bool
}

LoadResult is the value and existence result returned by a Loader.

type Loader

type Loader[K, V any] func(context.Context, K) (LoadResult[V], error)

Loader fetches a logical key from its source of truth.

type MutationResult

type MutationResult[K any] struct {
	Key K
	Err error
}

MutationResult reports one bulk mutation error in input order.

type Observer

type Observer interface {
	Observe(context.Context, Event) error
}

Observer receives best-effort semantic events from cache operations.

type Operation

type Operation string

Operation names the semantic cache action associated with an event or error.

const (
	// OperationGet identifies a read.
	OperationGet Operation = "get"
	// OperationSet identifies a write.
	OperationSet Operation = "set"
	// OperationDelete identifies invalidation.
	OperationDelete Operation = "delete"
	// OperationLoad identifies a source load.
	OperationLoad Operation = "load"
	// OperationEvict identifies capacity eviction.
	OperationEvict Operation = "evict"
	// OperationExpire identifies deadline expiration.
	OperationExpire Operation = "expire"
)

type Outcome

type Outcome string

Outcome is a low-cardinality semantic operation result.

const (
	// OutcomeSuccess identifies a successful mutation or load.
	OutcomeSuccess Outcome = "success"
	// OutcomeHit identifies a fresh read.
	OutcomeHit Outcome = "hit"
	// OutcomeMiss identifies an absent read.
	OutcomeMiss Outcome = "miss"
	// OutcomeStale identifies a stale read.
	OutcomeStale Outcome = "stale"
	// OutcomeNegative identifies a negative-cache result.
	OutcomeNegative Outcome = "negative"
	// OutcomeRejected identifies a failed conditional mutation.
	OutcomeRejected Outcome = "rejected"
	// OutcomeError identifies an operation failure.
	OutcomeError Outcome = "error"
	// OutcomeEvicted identifies capacity eviction.
	OutcomeEvicted Outcome = "evicted"
	// OutcomeExpired identifies deadline expiration.
	OutcomeExpired Outcome = "expired"
)

type OwnershipBackend

type OwnershipBackend interface {
	Backend
	SetIfOwned(context.Context, string, Record, OwnershipGuard) error
}

OwnershipBackend atomically validates ownership and writes one record.

type OwnershipGuard

type OwnershipGuard interface {
	StorageKey() string
	Owner() string
	Token() string
}

OwnershipGuard identifies backend-authenticated ownership for one protected write. Implementations must return opaque storage coordinates, not logical keys or credentials.

type RandomJitter

type RandomJitter struct{}

RandomJitter samples a non-cryptographic duration in [0, max).

func (RandomJitter) Duration

func (RandomJitter) Duration(upperBound time.Duration) time.Duration

Duration returns a non-negative duration below max, or zero for max <= 0.

type Record

type Record struct {
	Payload   []byte
	ExpiresAt time.Time
	StaleAt   time.Time
	Negative  bool
}

Record is the portable value and expiration envelope stored by a Backend.

func (Record) Clone

func (r Record) Clone() Record

Clone returns a portable record whose payload does not alias the receiver.

func (Record) Validate

func (r Record) Validate() error

Validate checks deadline portability and negative-record invariants.

type Result

type Result[V any] struct {
	State    State
	Value    V
	Negative bool
}

Result is the explicit outcome of a typed cache read.

type State

type State uint8

State describes whether a cache lookup is fresh, absent, or stale.

const (
	// Hit indicates a fresh cached value.
	Hit State = iota + 1
	// Miss indicates no usable cached value.
	Miss
	// Stale indicates a decoded value beyond its fresh TTL but within StaleFor.
	Stale
)

type StringKeyEncoder

type StringKeyEncoder struct{}

StringKeyEncoder encodes strings without conversion.

func (StringKeyEncoder) EncodeKey

func (StringKeyEncoder) EncodeKey(key string) ([]byte, error)

EncodeKey returns the UTF-8 bytes of key.

type SystemClock

type SystemClock struct{}

SystemClock uses the system wall clock.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns the current system time.

type TTLPolicy

type TTLPolicy struct {
	TTL      time.Duration
	StaleFor time.Duration
	Sliding  bool
}

TTLPolicy defines the fresh and stale lifetime of positive records.

func (TTLPolicy) Validate

func (p TTLPolicy) Validate() error

Validate reports whether the TTL and stale window are well formed.

Directories

Path Synopsis
backend
memory
Package memory provides a bounded, concurrency-safe LRU cache backend.
Package memory provides a bounded, concurrency-safe LRU cache backend.
redis
Package redis adapts go-redis/v9 clients to the cache backend contract.
Package redis adapts go-redis/v9 clients to the cache backend contract.
valkey
Package valkey adapts valkey-go clients to the cache backend contract.
Package valkey adapts valkey-go clients to the cache backend contract.
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
Package cachetest provides a shared backend conformance suite.
Package cachetest provides a shared backend conformance suite.
internal
wire
Package wire encodes portable backend records for remote storage.
Package wire encodes portable backend records for remote storage.
observability
otel
Package otel exports cache events as low-cardinality OpenTelemetry metrics.
Package otel exports cache events as low-cardinality OpenTelemetry metrics.
slog
Package slog records redacted cache events with the standard log package.
Package slog records redacted cache events with the standard log package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL