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):
| Driver | Status | Backing Store | Use Case |
|---|---|---|---|
inmemory | Implemented | sync.RWMutex + map[string]*cacheItem | Development, single-instance deployments |
redis | Declared (not yet implemented) | Redis server | Production, multi-instance |
memcached | Declared (not yet implemented) | Memcached server | High-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:
- First, it looks for an expired entry to remove.
- 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:
| Counter | Emitted When |
|---|---|
cache_hit | Get() finds a valid, non-expired entry |
cache_miss | Get() finds no entry or an expired one |
cache_set | Set() stores a value |
cache_delete | Delete() removes a key |
cache_clear | Clear() removes all keys |
cache_evict | An 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
| Interface | Methods | Purpose |
|---|---|---|
CounterCache | Incr, IncrBy, Decr, DecrBy | Atomic integer counters |
MultiCache | MGet, MSet, MDelete | Batch operations across multiple keys |
These interfaces are declared but not yet implemented in the in-memory backend.
Sentinel Errors
| Error | Meaning |
|---|---|
ErrNotFound | Key does not exist or has expired |
ErrNotConnected | Backend is not connected (call Connect first) |
ErrKeyTooLarge | Key exceeds MaxKeySize bytes |
ErrValueTooLarge | Value exceeds MaxValueSize bytes |
ErrInvalidTTL | Negative or otherwise invalid TTL |
ErrUnsupportedOperation | Operation not supported by the current backend |
How is this guide?