Generate Redis-backed Go cache. Invoke whenever user mentions cache or Redis.
Generate two files for every cache: a port interface and a Redis-backed implementation.
Pick before writing anything:
| Scenario | Variant | Get return type |
|---|---|---|
| Flag, existence check, rate limit | Boolean flag | bool |
| Structured data — tokens, sessions, profiles | JSON data | *dto.XxxData |
For TTL:
Every cache requires exactly two files:
internal/modules/<module>/ports/<cache_name>_cache.gointernal/modules/<module>/cache/<cache_name>_cache.goXxxCache)NewXxxCache)Set, Get, Delete)buildKey, calculateTTL)Use when caching simple existence flags, presence checks, or rate limit states.
"1" as the valuefalse, nil when the key doesn't exist (not an error)package ports
import "context"
// XxxCache describes ...
type XxxCache interface {
Set(ctx context.Context, id uint64) error
Get(ctx context.Context, id uint64) (bool, error)
Delete(ctx context.Context, id uint64) error
}
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
redislib "github.com/redis/go-redis/v9"
)
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
type EntityCache struct {
redisClient redis.UniversalClient
}
var _ ports.EntityCache = (*EntityCache)(nil)
func NewEntityCache(redisClient redis.UniversalClient) *EntityCache {
return &EntityCache{
redisClient: redisClient,
}
}
func (c *EntityCache) Set(ctx context.Context, id uint64) error {
key := c.buildKey(id)
return c.redisClient.Set(ctx, key, "1", entityCacheTTL).Err()
}
func (c *EntityCache) Get(ctx context.Context, id uint64) (bool, error) {
key := c.buildKey(id)
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redislib.Nil) {
return false, nil
}
return false, err
}
return true, nil
}
func (c *EntityCache) Delete(ctx context.Context, id uint64) error {
key := c.buildKey(id)
return c.redisClient.Del(ctx, key).Err()
}
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", entityCacheKeyPrefix, id)
}
Use when caching structured data. Data structs are defined in the dto package, never in ports.
json.Marshal before storingjson.Unmarshal when retrievingnil, nil on missing key — unless the key is always expected to exist, in which case return a domain error (e.g., errs.ErrXxxNotFound)getErr, unmarshalErr) to avoid shadowingpackage ports
import (
"context"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
}
// XxxCache describes ...
type XxxCache interface {
Set(ctx context.Context, key string, data dto.XxxData) error
Get(ctx context.Context, key string) (dto.XxxData, error)
Delete(ctx context.Context, key string) error
}
package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
redislib "github.com/redis/go-redis/v9"
)
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
type EntityCache struct {
redisClient redis.UniversalClient
}
var _ ports.EntityCache = (*EntityCache)(nil)
func NewEntityCache(redisClient redis.UniversalClient) *EntityCache {
return &EntityCache{
redisClient: redisClient,
}
}
func (c *EntityCache) Set(ctx context.Context, key string, data dto.EntityData) error {
cacheKey := c.buildKey(key)
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshal entity data: %w", err)
}
return c.redisClient.Set(ctx, cacheKey, jsonData, entityCacheTTL).Err()
}
func (c *EntityCache) Get(ctx context.Context, key string) (dto.EntityData, error) {
cacheKey := c.buildKey(key)
result := c.redisClient.Get(ctx, cacheKey)
if getErr := result.Err(); getErr != nil {
if errors.Is(getErr, redislib.Nil) {
return dto.EntityData{}, nil
}
return dto.EntityData{}, getErr
}
jsonData, err := result.Bytes()
if err != nil {
return dto.EntityData{}, fmt.Errorf("get bytes: %w", err)
}
var entityData dto.EntityData
if unmarshalErr := json.Unmarshal(jsonData, &entityData); unmarshalErr != nil {
return dto.EntityData{}, fmt.Errorf("unmarshal entity data: %w", unmarshalErr)
}
return entityData, nil
}
func (c *EntityCache) Delete(ctx context.Context, key string) error {
cacheKey := c.buildKey(key)
return c.redisClient.Del(ctx, cacheKey).Err()
}
func (c *EntityCache) buildKey(key string) string {
return entityCacheKeyPrefix + key
}
String ID (simple concatenation):
func (c *EntityCache) buildKey(id string) string {
return entityCacheKeyPrefix + id
}
Uint64 ID:
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", entityCacheKeyPrefix, id)
}
Composite key:
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
return fmt.Sprintf("%s%d:%s", entityCacheKeyPrefix, userID, resourceID)
}
Fixed TTL — for short-lived data where stampede is not a concern:
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTL = 10 * time.Minute
)
Randomized TTL — for long-lived data created in bulk (prevents cache stampede):
import "math/rand"
const (
entityCacheKeyPrefix = "entity_name:"
entityCacheTTLMin = 23 * time.Hour
entityCacheTTLMax = 25 * time.Hour
)
func (c *EntityCache) calculateTTL() time.Duration {
min := entityCacheTTLMin.Milliseconds()
max := entityCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
Common TTL ranges:
5-15 minutes — OTP codes, OAuth state, rate limits50-70 minutes — User sessions12-25 hours — Activation flags, daily metrics6.5-7.5 days — Weekly aggregationsXxxCache (ports package, no suffix)XxxCache (cache package — same name, disambiguated by package)NewXxxCache, returns *XxxCacheentityCacheKeyPrefix, entityCacheTTL)Add to internal/modules/<module>/fx.go:
fx.Provide(
fx.Annotate(
cache.NewXxxCache,
fx.As(new(ports.XxxCache)),
),
),
redis.UniversalClient from "github.com/cristiano-pacheco/bricks/pkg/redis"redislib "github.com/redis/go-redis/v9" for nil detectionports/, implementation in cache/var _ ports.XxxCache = (*XxxCache)(nil) immediately below the struct*XxxCache (pointer)ctx context.Context as first parameter — never call context.Background() internallyredislib "github.com/redis/go-redis/v9" and check with errors.Is(err, redislib.Nil)buildKey() helper; + for string IDs, fmt.Sprintf for numeric IDsfalse, nil; JSON cache returns nil, nil (or a domain error if the key must exist)dto/, never defined inline in ports/"action noun: %w" format (e.g., "marshal oauth state: %w", "get bytes: %w")ports/<name>_cache.gocache/<name>_cache.gofx.gomake lintmake nilaway