cache

package module
v1.13.5 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

Forge Cache, an in-memory key-value cache extension for Go applications built on Forge

Forge Cache: in-memory caching for Go services

Forge Cache is the caching extension for Forge, the Go backend framework. You add it to your app and get a key-value cache with per-key TTLs, a default expiry, key prefixes for namespacing, glob-style key lookup and size limits, registered in the dependency injection container so any service can ask for it. Today the in-memory driver is the one that works. The config also accepts redis and memcached, but those drivers aren't implemented yet, so read Drivers before you plan around them.

Go CI Go Reference Apache 2.0

Documentation · Quick start · Features · Using the cache · Drivers · Configuration · FAQ

Quick start

You need Go 1.26 or later and a Forge app. Add the extension module:

go get github.com/xraph/forge@latest
go get github.com/xraph/forge/extensions/cache@latest

Save this as main.go:

package main

import (
	"errors"
	"log"
	"net/http"
	"time"

	"github.com/xraph/forge"
	"github.com/xraph/forge/extensions/cache"
)

func main() {
	app := forge.NewApp(forge.AppConfig{
		Name:        "cache-demo",
		HTTPAddress: ":8080",
		Extensions: []forge.Extension{
			cache.NewExtension(
				cache.WithDefaultTTL(10*time.Minute),
				cache.WithPrefix("demo:"),
			),
		},
	})

	err := app.Router().GET("/greeting", func(ctx forge.Context) error {
		c, err := cache.GetCache(app.Container())
		if err != nil {
			return err
		}

		value, err := c.Get(ctx.Context(), "greeting")
		if errors.Is(err, cache.ErrNotFound) {
			value = []byte("Hello from the cache")
			if err := c.Set(ctx.Context(), "greeting", value, 30*time.Second); err != nil {
				return err
			}

			return ctx.JSON(http.StatusOK, map[string]string{"source": "miss", "value": string(value)})
		}
		if err != nil {
			return err
		}

		return ctx.JSON(http.StatusOK, map[string]string{"source": "hit", "value": string(value)})
	})
	if err != nil {
		log.Fatal(err)
	}

	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}

Run it and call the route twice:

go mod tidy
go run .

curl http://localhost:8080/greeting
# {"source":"miss","value":"Hello from the cache"}
curl http://localhost:8080/greeting
# {"source":"hit","value":"Hello from the cache"}

The second call comes from memory. Wait thirty seconds and you get a miss again, because the handler wrote that key with its own 30-second TTL and that wins over the ten-minute default you set on the extension. When you stop the app with Ctrl+C, Forge stops the cache service along with everything else, and the in-memory store is emptied.

Features

You need Forge Cache gives you
A cache any service can inject A cache.Cache interface and a *cache.CacheService registered in the Vessel container, also under the name "cache"
Entries that expire on their own A TTL per Set, a DefaultTTL for Set calls that pass 0, plus TTL and Expire to read and change it later
Several apps or tenants sharing a key space A Prefix added to every key on write and removed again from Keys results
Bounded memory use MaxSize (entries), MaxKeySize and MaxValueSize (bytes), checked on every Set
Finding keys by pattern Keys(ctx, "user:*") with * wildcards
Strings and structs without hand-written encoding StringCache and JSONCache helpers on the in-memory backend
To know it's working Hit, miss, set, delete, clear and eviction counters through Forge metrics, and a Health method on the service that pings the backend

Using the cache

Inject it into your own services

The extension registers a constructor, so you can take cache.Cache as a parameter like any other dependency:

type UserService struct {
	cache cache.Cache
}

func NewUserService(c cache.Cache) *UserService {
	return &UserService{cache: c}
}

// In your setup code, after forge.NewApp:
err := forge.Provide(app.Container(), NewUserService)

If you'd rather pull it out yourself, cache.GetCache(container), cache.GetCacheFromApp(app) and forge.Inject[cache.Cache](container) all return the same singleton. MustGetCache and MustGetCacheFromApp panic when it isn't registered.

The Cache interface

Every value is a []byte. These methods are on cache.Cache:

Method What it does
Get(ctx, key) Returns the value, or cache.ErrNotFound when the key is missing or expired
Set(ctx, key, value, ttl) Stores a copy of value. A ttl of 0 uses DefaultTTL
Delete(ctx, key) Removes a key. Deleting a missing key is not an error
Exists(ctx, key) Reports whether a live entry exists
Keys(ctx, pattern) Lists live keys matching a glob such as *, user:*, *:draft or user:*:profile
TTL(ctx, key) Returns the time left, or 0 for an entry that never expires
Expire(ctx, key, ttl) Sets a new TTL. A ttl of 0 or less makes the entry permanent
Clear(ctx) Empties the whole cache
Ping(ctx) Returns cache.ErrNotConnected until the service has started

Connect and Disconnect are on the interface too. You don't call them: the extension does that for you during start and stop.

Two things to know about Keys. It supports one * per pattern, and a pattern with two wildcards matches nothing. It also walks every entry while holding the read lock, so if your cache is large you should keep it off hot request paths. A typical cleanup looks like this:

keys, err := c.Keys(ctx, "session:*")
if err != nil {
	return err
}

for _, k := range keys {
	if err := c.Delete(ctx, k); err != nil {
		return err
	}
}
Strings and JSON

The in-memory backend also implements cache.StringCache (GetString, SetString) and cache.JSONCache (GetJSON, SetJSON). The cache.Cache you get from the container doesn't expose them directly, so ask for the service and type-assert its backend:

svc, err := forge.Inject[*cache.CacheService](app.Container())
if err != nil {
	return err
}

jc, ok := svc.Cache().(cache.JSONCache)
if !ok {
	return errors.New("backend does not support JSON helpers")
}

if err := jc.SetJSON(ctx, "user:42", User{ID: "42", Name: "Ada"}, time.Hour); err != nil {
	return err
}

var u User
err = jc.GetJSON(ctx, "user:42", &u)

cache.CounterCache (Incr, DecrBy and friends) and cache.MultiCache (MGet, MSet, MDelete) are declared in the package, but no backend implements them yet.

Errors
Error When you'll see it
cache.ErrNotFound Get, TTL or Expire on a key that is missing or expired
cache.ErrNotConnected Any call before the app has started the cache, or after it stopped
cache.ErrKeyTooLarge A key longer than MaxKeySize bytes
cache.ErrValueTooLarge A value larger than MaxValueSize bytes
cache.ErrInvalidTTL A negative DefaultTTL in the config

Compare them with errors.Is. An empty key on Set returns a plain error.

Drivers

Driver Status
inmemory Works. A map guarded by a read-write mutex, with a background sweep every CleanupInterval that removes expired entries
redis Accepted by config validation (it needs a URL), but service construction fails with "redis driver not yet implemented"
memcached Same as redis: validated, then construction fails with "memcached driver not yet implemented"

So for now the cache lives in one process. If you run two replicas of your service, each has its own copy, and a restart empties it.

When the in-memory cache holds MaxSize entries, the next Set removes one entry first. It picks an expired entry if it finds one, and otherwise an arbitrary entry in Go map order. That is not LRU. If you need hot keys to survive, size MaxSize so you rarely reach it.

Configuration

You can configure the extension in code with options, or in YAML under extensions.cache (the extension also reads a top-level cache key).

Options
Option Sets
WithDriver(string) Driver: inmemory, redis or memcached
WithURL(string) URL, required for redis and memcached
WithDefaultTTL(time.Duration) DefaultTTL
WithMaxSize(int) MaxSize
WithPrefix(string) Prefix
WithConnectionPoolSize(int) ConnectionPoolSize
WithRequireConfig(bool) RequireConfig: fail at startup if no YAML config is found
WithConfig(cache.Config) Replaces the whole config

NewExtensionWithConfig(cfg) is the same as NewExtension(WithConfig(cfg)).

Fields and defaults
Field Default (DefaultConfig()) Notes
Driver inmemory
URL empty
DefaultTTL 5m Used when Set gets a TTL of 0
MaxSize 10000 Entries. 0 means no limit
CleanupInterval 1m How often expired entries are swept
MaxKeySize 250 Bytes. 0 means no limit
MaxValueSize 1048576 Bytes (1 MiB). 0 means no limit
Prefix empty
ConnectionPoolSize 10 Reserved for network drivers
ConnectionTimeout, ReadTimeout, WriteTimeout 5s, 3s, 3s Reserved for network drivers
RequireConfig false
YAML

The keys are the snake_case field names:

extensions:
  cache:
    driver: inmemory
    default_ttl: 10m
    max_size: 50000
    prefix: "orders:"

If your file still says MaxSize or DefaultTTL, it keeps working, and you'll get a warning at startup naming the snake_case key to switch to.

A plain cache.NewExtension() picks up every key in that block. YAML overrides DefaultConfig() and options you set in code override YAML, and fields you leave out of the file keep their defaults.

Forge reads config.yaml only when EnableConfigAutoDiscovery is on. forge.DefaultAppConfig() turns it on, but an AppConfig literal like the one in the quick start leaves it off, so set it yourself or pass your own ConfigManager.

Documentation

Topic Where
Features and usage Cache features
Configuration reference Cache configuration
Function reference Cache functions
Go API pkg.go.dev
How extensions load config and register services Forge extension system

The docs pages were written before this README, so where they disagree about drivers or config keys, the tables above match the code.

FAQ

How do I add caching to a Go web app built on Forge?

Add cache.NewExtension() to Extensions in forge.AppConfig, then inject cache.Cache wherever you need it. The quick start is a complete program.

Does Forge Cache support Redis or Memcached?

Not yet. Both driver names pass config validation, but the service returns a "not yet implemented" error when it's constructed, so the app fails to start. Use inmemory until a network driver lands.

Is the in-memory cache safe for concurrent use?

Yes. All reads and writes go through a sync.RWMutex, and the extension's tests run concurrent goroutines against it.

What happens when the cache is full?

When it holds MaxSize entries, the next Set removes one expired entry, or an arbitrary entry if none has expired, and then stores the new one. Set MaxSize to 0 for no limit.

Can I store structs in the cache?

Yes, through JSONCache.SetJSON and GetJSON on the backend, as shown in Strings and JSON. Otherwise you encode to []byte yourself.

Contributing

The cache is its own Go module inside the Forge repository. Build, vet and test it from its directory the way CI does:

cd extensions/cache
GOWORK=off go build ./...
GOWORK=off go vet ./...
GOWORK=off go test -short -count=1 ./...
golangci-lint run ./...

Forge's AGENTS.md has the pinned lint version and the rest of the repository conventions. Commits follow Conventional Commits, for example fix(cache): ....

License

Forge Cache is part of Forge and is available under the Apache License 2.0.

Documentation

Index

Constants

View Source
const (
	// ServiceKey is the DI key for the cache service.
	ServiceKey = "cache"
)

DI container keys for cache extension services.

Variables

View Source
var (
	// ErrNotFound is returned when a key is not found in the cache.
	ErrNotFound = errors.New("cache: key not found")

	// ErrNotConnected is returned when attempting operations on a disconnected cache.
	ErrNotConnected = errors.New("cache: not connected")

	// ErrInvalidTTL is returned when an invalid TTL is provided.
	ErrInvalidTTL = errors.New("cache: invalid TTL")

	// ErrKeyTooLarge is returned when a key exceeds the maximum allowed size.
	ErrKeyTooLarge = errors.New("cache: key too large")

	// ErrValueTooLarge is returned when a value exceeds the maximum allowed size.
	ErrValueTooLarge = errors.New("cache: value too large")

	// ErrUnsupportedOperation is returned when an operation is not supported by the backend.
	ErrUnsupportedOperation = errors.New("cache: operation not supported")
)

Functions

func NewExtension

func NewExtension(opts ...ConfigOption) forge.Extension

NewExtension creates a new cache extension with functional options. Config is loaded from ConfigManager by default, with options providing overrides.

Example:

// Load from ConfigManager (tries "extensions.cache", then "cache")
cache.NewExtension()

// Override specific fields
cache.NewExtension(
    cache.WithDriver("redis"),
    cache.WithURL("redis://localhost:6379"),
)

// Require config from ConfigManager
cache.NewExtension(cache.WithRequireConfig(true))

func NewExtensionWithConfig

func NewExtensionWithConfig(config Config) forge.Extension

NewExtensionWithConfig creates a new cache extension with a complete config. This is for backward compatibility or when config is fully known at initialization.

Types

type Cache

type Cache interface {
	// Connect establishes a connection to the cache backend
	Connect(ctx context.Context) error

	// Disconnect closes the connection to the cache backend
	Disconnect(ctx context.Context) error

	// Ping checks if the cache backend is accessible
	Ping(ctx context.Context) error

	// Get retrieves a value from the cache by key
	// Returns ErrNotFound if the key doesn't exist
	Get(ctx context.Context, key string) ([]byte, error)

	// Set stores a value in the cache with the given key and TTL
	// If ttl is 0, uses the default TTL from config
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

	// Delete removes a key from the cache
	Delete(ctx context.Context, key string) error

	// Exists checks if a key exists in the cache
	Exists(ctx context.Context, key string) (bool, error)

	// Clear removes all keys from the cache
	Clear(ctx context.Context) error

	// Keys returns all keys matching the pattern
	// Pattern uses glob-style matching (* for wildcard)
	Keys(ctx context.Context, pattern string) ([]string, error)

	// TTL returns the remaining time-to-live for a key
	// Returns 0 if the key has no expiration
	TTL(ctx context.Context, key string) (time.Duration, error)

	// Expire sets a new TTL for a key
	Expire(ctx context.Context, key string, ttl time.Duration) error
}

Cache is the main interface for cache operations. All cache backends (in-memory, Redis, Memcached) implement this interface.

func GetCache

func GetCache(c forge.Container) (Cache, error)

GetCache retrieves the Cache service from the container. Returns error if not found or type assertion fails.

func GetCacheFromApp

func GetCacheFromApp(app forge.App) (Cache, error)

GetCacheFromApp retrieves the Cache service from the app. Returns error if not found or type assertion fails.

func MustGetCache

func MustGetCache(c forge.Container) Cache

MustGetCache retrieves the Cache service from the container. Panics if not found or type assertion fails.

func MustGetCacheFromApp

func MustGetCacheFromApp(app forge.App) Cache

MustGetCacheFromApp retrieves the Cache service from the app. Panics if not found or type assertion fails.

type CacheService

type CacheService struct {
	// contains filtered or unexported fields
}

CacheService wraps a Cache implementation and provides lifecycle management. It implements vessel's di.Service interface so Vessel can manage its lifecycle.

func NewCacheService

func NewCacheService(config Config, logger forge.Logger, metrics forge.Metrics) (*CacheService, error)

NewCacheService creates a new cache service with the given configuration. This is the constructor that will be registered with the DI container.

func (*CacheService) Cache

func (s *CacheService) Cache() Cache

Cache returns the underlying cache implementation. This allows services to use the cache directly.

func (*CacheService) Clear

func (s *CacheService) Clear(ctx context.Context) error

func (*CacheService) Connect

func (s *CacheService) Connect(ctx context.Context) error

func (*CacheService) Delete

func (s *CacheService) Delete(ctx context.Context, key string) error

func (*CacheService) Disconnect

func (s *CacheService) Disconnect(ctx context.Context) error

func (*CacheService) Exists

func (s *CacheService) Exists(ctx context.Context, key string) (bool, error)

func (*CacheService) Expire

func (s *CacheService) Expire(ctx context.Context, key string, ttl time.Duration) error

func (*CacheService) Get

func (s *CacheService) Get(ctx context.Context, key string) ([]byte, error)

func (*CacheService) Health

func (s *CacheService) Health(ctx context.Context) error

Health checks if the cache service is healthy. This is called by the health check system.

func (*CacheService) Keys

func (s *CacheService) Keys(ctx context.Context, pattern string) ([]string, error)

func (*CacheService) Name

func (s *CacheService) Name() string

Name returns the service name for Vessel's lifecycle management.

func (*CacheService) Ping

func (s *CacheService) Ping(ctx context.Context) error

func (*CacheService) Set

func (s *CacheService) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

func (*CacheService) Start

func (s *CacheService) Start(ctx context.Context) error

Start starts the cache service by connecting to the backend. This is called automatically by Vessel during container.Start().

func (*CacheService) Stop

func (s *CacheService) Stop(ctx context.Context) error

Stop stops the cache service by disconnecting from the backend. This is called automatically by Vessel during container.Stop().

func (*CacheService) TTL

func (s *CacheService) TTL(ctx context.Context, key string) (time.Duration, error)

type Config

type Config struct {
	// Driver specifies the cache backend: "inmemory", "redis", "memcached"
	Driver string `yaml:"driver"`

	// URL is the connection string for the cache backend
	// Examples:
	//   - Redis: "redis://localhost:6379/0"
	//   - Memcached: "memcached://localhost:11211"
	//   - In-Memory: "" (not used)
	URL string `yaml:"url"`

	// DefaultTTL is the default time-to-live for cache entries
	// Used when Set() is called with ttl=0
	DefaultTTL time.Duration `yaml:"default_ttl"`

	// MaxSize is the maximum number of items in the cache (in-memory only)
	// 0 means unlimited
	MaxSize int `yaml:"max_size"`

	// CleanupInterval is how often to clean up expired items (in-memory only)
	CleanupInterval time.Duration `yaml:"cleanup_interval"`

	// MaxKeySize is the maximum allowed key size in bytes
	MaxKeySize int `yaml:"max_key_size"`

	// MaxValueSize is the maximum allowed value size in bytes
	MaxValueSize int `yaml:"max_value_size"`

	// Prefix is prepended to all keys
	// Useful for namespacing in shared cache instances
	Prefix string `yaml:"prefix"`

	// ConnectionPoolSize is the number of connections to maintain (Redis/Memcached)
	ConnectionPoolSize int `yaml:"connection_pool_size"`

	// ConnectionTimeout is the timeout for establishing a connection
	ConnectionTimeout time.Duration `yaml:"connection_timeout"`

	// ReadTimeout is the timeout for read operations
	ReadTimeout time.Duration `yaml:"read_timeout"`

	// WriteTimeout is the timeout for write operations
	WriteTimeout time.Duration `yaml:"write_timeout"`

	// RequireConfig determines if config must exist in ConfigManager
	// If true, extension fails to start without config
	// If false, uses defaults when config is missing
	RequireConfig bool `yaml:"-"`
}

Config configures the cache extension.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a default configuration for in-memory cache.

func (Config) Validate

func (c Config) Validate() error

Validate validates the configuration.

type ConfigOption

type ConfigOption func(*Config)

ConfigOption is a functional option for configuring the cache extension.

func WithConfig

func WithConfig(config Config) ConfigOption

WithConfig applies a complete config.

func WithConnectionPoolSize

func WithConnectionPoolSize(size int) ConfigOption

WithConnectionPoolSize sets the connection pool size.

func WithDefaultTTL

func WithDefaultTTL(ttl time.Duration) ConfigOption

WithDefaultTTL sets the default TTL.

func WithDriver

func WithDriver(driver string) ConfigOption

WithDriver sets the cache driver.

func WithMaxSize

func WithMaxSize(size int) ConfigOption

WithMaxSize sets the maximum cache size (in-memory only).

func WithPrefix

func WithPrefix(prefix string) ConfigOption

WithPrefix sets the key prefix.

func WithRequireConfig

func WithRequireConfig(require bool) ConfigOption

WithRequireConfig sets whether config is required from ConfigManager.

func WithURL

func WithURL(url string) ConfigOption

WithURL sets the cache URL.

type CounterCache

type CounterCache interface {
	Incr(ctx context.Context, key string) (int64, error)
	IncrBy(ctx context.Context, key string, value int64) (int64, error)
	Decr(ctx context.Context, key string) (int64, error)
	DecrBy(ctx context.Context, key string, value int64) (int64, error)
}

CounterCache provides atomic increments and decrements.

type Extension

type Extension struct {
	*forge.BaseExtension
	// contains filtered or unexported fields
}

Extension implements forge.Extension for cache functionality. The extension is now a lightweight facade that loads config and registers services. Service lifecycle is managed by Vessel, not by the extension.

func (*Extension) Health

func (e *Extension) Health(ctx context.Context) error

Health checks if the cache is healthy. This delegates to the CacheService health check managed by Vessel.

func (*Extension) Register

func (e *Extension) Register(app forge.App) error

Register registers the cache extension with the app. This method now only loads configuration and registers service constructors. Service lifecycle (Start/Stop) is managed by Vessel.

func (*Extension) Start

func (e *Extension) Start(ctx context.Context) error

Start resolves and starts the cache service, then marks the extension as started.

func (*Extension) Stop

func (e *Extension) Stop(ctx context.Context) error

Stop stops the cache service and marks the extension as stopped.

type InMemoryCache

type InMemoryCache struct {
	// contains filtered or unexported fields
}

InMemoryCache implements Cache interface using an in-memory store.

func NewInMemoryCache

func NewInMemoryCache(config Config, logger forge.Logger, metrics forge.Metrics) *InMemoryCache

NewInMemoryCache creates a new in-memory cache.

func (*InMemoryCache) Clear

func (c *InMemoryCache) Clear(ctx context.Context) error

Clear removes all keys from the cache.

func (*InMemoryCache) Connect

func (c *InMemoryCache) Connect(ctx context.Context) error

Connect initializes the cache and starts the cleanup goroutine.

func (*InMemoryCache) Delete

func (c *InMemoryCache) Delete(ctx context.Context, key string) error

Delete removes a key from the cache.

func (*InMemoryCache) Disconnect

func (c *InMemoryCache) Disconnect(ctx context.Context) error

Disconnect stops the cache and cleans up resources.

func (*InMemoryCache) Exists

func (c *InMemoryCache) Exists(ctx context.Context, key string) (bool, error)

Exists checks if a key exists in the cache.

func (*InMemoryCache) Expire

func (c *InMemoryCache) Expire(ctx context.Context, key string, ttl time.Duration) error

Expire sets a new TTL for a key.

func (*InMemoryCache) Get

func (c *InMemoryCache) Get(ctx context.Context, key string) ([]byte, error)

Get retrieves a value from the cache.

func (*InMemoryCache) GetJSON

func (c *InMemoryCache) GetJSON(ctx context.Context, key string, target any) error

func (*InMemoryCache) GetString

func (c *InMemoryCache) GetString(ctx context.Context, key string) (string, error)

func (*InMemoryCache) Keys

func (c *InMemoryCache) Keys(ctx context.Context, pattern string) ([]string, error)

Keys returns all keys matching the pattern.

func (*InMemoryCache) Ping

func (c *InMemoryCache) Ping(ctx context.Context) error

Ping checks if the cache is connected.

func (*InMemoryCache) Set

func (c *InMemoryCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

Set stores a value in the cache.

func (*InMemoryCache) SetJSON

func (c *InMemoryCache) SetJSON(ctx context.Context, key string, value any, ttl time.Duration) error

func (*InMemoryCache) SetString

func (c *InMemoryCache) SetString(ctx context.Context, key string, value string, ttl time.Duration) error

func (*InMemoryCache) TTL

func (c *InMemoryCache) TTL(ctx context.Context, key string) (time.Duration, error)

TTL returns the remaining time-to-live for a key.

type JSONCache

type JSONCache interface {
	GetJSON(ctx context.Context, key string, target any) error
	SetJSON(ctx context.Context, key string, value any, ttl time.Duration) error
}

JSONCache stores values as JSON and unmarshals them on read.

type MultiCache

type MultiCache interface {
	MGet(ctx context.Context, keys ...string) ([][]byte, error)
	MSet(ctx context.Context, kvs map[string][]byte, ttl time.Duration) error
	MDelete(ctx context.Context, keys ...string) error
}

MultiCache reads, writes and deletes several keys in one call.

type StringCache

type StringCache interface {
	GetString(ctx context.Context, key string) (string, error)
	SetString(ctx context.Context, key string, value string, ttl time.Duration) error
}

StringCache stores and retrieves plain string values.

Jump to

Keyboard shortcuts

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