Caching

go.putnami.dev/cache provides a generic caching layer with in-memory, disk, and layered implementations. All caches are safe for concurrent use.

Cache interface

All implementations share the same interface:

type Cache interface {
    Get(ctx context.Context, key string) ([]byte, bool)
    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
    Delete(ctx context.Context, key string) error
    Has(ctx context.Context, key string) bool
    Clear(ctx context.Context) error
    Close() error
}

Memory cache

In-memory cache with optional FIFO eviction:

import "go.putnami.dev/cache"

c := cache.NewMemoryCache(cache.MemoryConfig{
    MaxEntries:      1000,           // 0 = unlimited
    CleanupInterval: 5 * time.Minute,
})
defer c.Close()

Configuration

Field Type Default Description
MaxEntries int 0 Maximum entries (0 = unlimited). FIFO eviction when exceeded
CleanupInterval time.Duration Interval for removing expired entries

Disk cache

Filesystem-backed persistent cache that survives process restarts:

c, err := cache.NewDiskCache(cache.DiskConfig{
    Dir:             "./cache",
    CleanupInterval: 10 * time.Minute,
})
if err != nil {
    log.Fatal(err)
}
defer c.Close()

Objects are stored as files with SHA-256 hashed filenames. The directory is created automatically if it doesn't exist.

Configuration

Field Type Default Description
Dir string Cache directory (created if needed)
CleanupInterval time.Duration Interval for removing expired entries

Layered cache

Compose an L1/L2 cache hierarchy. On a cache miss in L1, the value is fetched from L2 and promoted to L1:

memory := cache.NewMemoryCache(cache.MemoryConfig{MaxEntries: 100})
disk, _ := cache.NewDiskCache(cache.DiskConfig{Dir: "./cache"})

c := cache.NewLayeredCache(memory, disk)
defer c.Close()

Behavior

Operation L1 (memory) L2 (disk)
Get Check first; on miss, fetch from L2 and promote Checked on L1 miss
Set Write Write
Delete Delete Delete
Clear Clear Clear

Operations

Set and get

// Store with TTL
err := c.Set(ctx, "user:123", []byte(`{"name":"Jane"}`), 5*time.Minute)

// Store without expiration
err := c.Set(ctx, "config", data, 0)

// Retrieve
value, found := c.Get(ctx, "user:123")
if !found {
    // cache miss or expired
}

Check and delete

if c.Has(ctx, "user:123") {
    // key exists and is not expired
}

err := c.Delete(ctx, "user:123")

// Clear all entries
err := c.Clear(ctx)

Patterns

Cache-aside

func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    key := "user:" + id

    // Check cache
    if data, found := s.cache.Get(ctx, key); found {
        var user User
        json.Unmarshal(data, &user)
        return &user, nil
    }

    // Cache miss — fetch from database
    user, err := s.repo.FindByID(ctx, "id", id)
    if err != nil {
        return nil, err
    }

    // Store in cache
    data, _ := json.Marshal(user)
    s.cache.Set(ctx, key, data, 10*time.Minute)

    return &user, nil
}

Cache invalidation

func (s *UserService) UpdateUser(ctx context.Context, id string, input UpdateInput) error {
    if err := s.repo.Update(ctx, id, input); err != nil {
        return err
    }
    // Invalidate cached entry
    return s.cache.Delete(ctx, "user:"+id)
}