Cache

Features

Cache extension capabilities and backend details

Unified Cache Interface

All backends implement the same Cache interface, so application code is backend-agnostic. Swapping from in-memory to Redis requires only a config change -- no code modifications.

type Cache interface {
    Connect(ctx context.Context) error
    Disconnect(ctx context.Context) error
    Ping(ctx context.Context) error
    Get(ctx context.Context, key string) ([]byte, error)
    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
    Delete(ctx context.Context, key string) error
    Exists(ctx context.Context, key string) (bool, error)
    Clear(ctx context.Context) error
    Keys(ctx context.Context, pattern string) ([]string, error)
    TTL(ctx context.Context, key string) (time.Duration, error)
    Expire(ctx context.Context, key string, ttl time.Duration) error
}

Multiple Backends

Switch between backends using a single config field (Driver):

DriverStatusBacking StoreUse Case
inmemoryImplementedsync.RWMutex + map[string]*cacheItemDevelopment, single-instance deployments
redisDeclared (not yet implemented)Redis serverProduction, multi-instance
memcachedDeclared (not yet implemented)Memcached serverHigh-throughput, distributed

TTL-Based Expiration

Every entry can have an individual TTL. When ttl == 0 is passed to Set(), the configured DefaultTTL (default: 5 minutes) is applied automatically. You can also remove expiration entirely by calling Expire() with a non-positive TTL.

// Set with explicit TTL
c.Set(ctx, "token:abc", tokenBytes, 1*time.Hour)

// Set with default TTL from config
c.Set(ctx, "token:def", tokenBytes, 0)

// Inspect remaining TTL
remaining, _ := c.TTL(ctx, "token:abc")

// Update TTL on existing key
c.Expire(ctx, "token:abc", 2*time.Hour)

Automatic Cleanup

The in-memory backend runs a background goroutine that sweeps expired entries at the configured CleanupInterval (default: 1 minute). This prevents unbounded memory growth from expired-but-unaccessed entries. The goroutine starts on Connect() and stops cleanly on Disconnect().

Eviction

When MaxSize is reached (default: 10,000 items), the in-memory backend evicts entries on write:

  1. First, it looks for an expired entry to remove.
  2. If no expired entries exist, it removes the first entry found (map iteration order).

Each eviction emits a cache_evict counter metric.

Key Prefixing

All keys are transparently prefixed with Config.Prefix, enabling safe multi-tenant or multi-service usage of shared backends. Prefixes are stripped when returning keys from Keys().

// With Prefix "myapp:" configured, this stores as "myapp:user:123"
c.Set(ctx, "user:123", data, 0)

// Keys() returns "user:123" (prefix stripped)
keys, _ := c.Keys(ctx, "user:*")

Size Guards

MaxKeySize (default: 250 bytes) and MaxValueSize (default: 1 MB) are validated on every write. If either limit is exceeded, the operation returns ErrKeyTooLarge or ErrValueTooLarge without storing the data.

Glob-Style Key Listing

Keys(ctx, pattern) supports * wildcard patterns for key discovery:

  • "*" -- matches all keys
  • "user:*" -- prefix match
  • "*:sessions" -- suffix match
  • "app:*:config" -- prefix + suffix match

Metrics

The in-memory backend emits counters via the forge.Metrics interface:

CounterEmitted When
cache_hitGet() finds a valid, non-expired entry
cache_missGet() finds no entry or an expired one
cache_setSet() stores a value
cache_deleteDelete() removes a key
cache_clearClear() removes all keys
cache_evictAn entry is evicted due to MaxSize

Health Checks

Ping() verifies the backend is connected and responsive. The CacheService.Health() method delegates to Ping(), which Vessel calls during health check cycles. If the cache is not connected, Ping() returns ErrNotConnected.

Extended Interfaces (In-Memory)

The in-memory backend implements additional typed interfaces beyond the base Cache:

StringCache

Store and retrieve string values without manual []byte conversion:

inmem := c.Cache().(*InMemoryCache) // type assert to access extended methods
inmem.SetString(ctx, "greeting", "hello", 5*time.Minute)
val, _ := inmem.GetString(ctx, "greeting") // "hello"

JSONCache

Serialize/deserialize any Go value as JSON:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

inmem.SetJSON(ctx, "user:1", User{Name: "Alice", Email: "alice@example.com"}, 0)

var user User
inmem.GetJSON(ctx, "user:1", &user) // user.Name == "Alice"

CounterCache and MultiCache

InterfaceMethodsPurpose
CounterCacheIncr, IncrBy, Decr, DecrByAtomic integer counters
MultiCacheMGet, MSet, MDeleteBatch operations across multiple keys

These interfaces are declared but not yet implemented in the in-memory backend.

Sentinel Errors

ErrorMeaning
ErrNotFoundKey does not exist or has expired
ErrNotConnectedBackend is not connected (call Connect first)
ErrKeyTooLargeKey exceeds MaxKeySize bytes
ErrValueTooLargeValue exceeds MaxValueSize bytes
ErrInvalidTTLNegative or otherwise invalid TTL
ErrUnsupportedOperationOperation not supported by the current backend

How is this guide?

On this page