Go Create Cache
Generate cache files for Go backend using Redis.
Two-File Pattern
Every cache requires two files:
- •Port interface:
internal/modules/<module>/ports/<cache_name>_cache.go - •Cache implementation:
internal/modules/<module>/cache/<cache_name>_cache.go
Port File Layout Order
- •Interface definition (
XxxCache— no suffix)
Cache File Layout Order
- •Constants (cache key prefix, TTL)
- •Implementation struct (
XxxCache) - •Compile-time interface assertion
- •Constructor (
NewXxxCache) - •Methods (
Set,Get,Delete, etc.) - •Helper methods (
buildKey, etc.)
Port Interface Structure
Location: internal/modules/<module>/ports/<cache_name>_cache.go
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Cache Implementation Structure
Location: internal/modules/<module>/cache/<cache_name>_cache.go
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/<module>/ports"
)
const (
cacheKeyPrefix = "entity_name:"
cacheTTLMin = 23 * time.Hour
cacheTTLMax = 25 * time.Hour
)
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(id uint64) error {
key := c.buildKey(id)
ctx := context.Background()
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, "1", ttl).Err()
}
func (c *EntityCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *EntityCache) Get(id uint64) (bool, error) {
key := c.buildKey(id)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil // Key does not exist
}
return false, err
}
return true, nil
}
func (c *EntityCache) Delete(id uint64) error {
key := c.buildKey(id)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
Cache Variants
Boolean flag cache (Set/Get/Delete)
Use when caching simple existence or state flags.
Port (ports/user_activated_cache.go):
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Implementation notes:
- •Store
"1"as value for true state - •Return
false, nilwhen key doesn't exist (not an error) - •Use
errors.Is(err, redisClient.Nil)to detect missing keys
Value cache (Set/Get/Delete with data)
Use when caching structured data or strings.
Port (ports/session_cache.go):
type SessionCache interface {
Set(sessionID string, data SessionData) error
Get(sessionID string) (*SessionData, error)
Delete(sessionID string) error
}
Implementation notes:
- •Serialize data with
json.Marshalbefore storing - •Deserialize with
json.Unmarshalwhen retrieving - •Return
nil, nilwhen key doesn't exist (not an error) - •TTL is internal to the cache implementation with randomized range to prevent cache stampede
Redis Client Usage
The cache uses redis.UniversalClient directly from the Bricks Redis package (github.com/cristiano-pacheco/bricks/pkg/redis).
Common operations:
- •
Set(ctx, key, value, ttl)- Store value with TTL - •
Get(ctx, key)- Retrieve value - •
Del(ctx, key)- Delete key - •
Exists(ctx, key)- Check if key exists - •
Incr(ctx, key)- Increment counter - •
Expire(ctx, key, ttl)- Set TTL on existing key
Key Building
Always use a helper method to build cache keys consistently:
func (c *EntityCache) buildKey(id uint64) string {
return fmt.Sprintf("%s%d", cacheKeyPrefix, id)
}
For string IDs:
func (c *EntityCache) buildKey(id string) string {
return fmt.Sprintf("%s%s", cacheKeyPrefix, id)
}
For composite keys:
func (c *EntityCache) buildKey(userID uint64, resourceID string) string {
return fmt.Sprintf("%s%d:%s", cacheKeyPrefix, userID, resourceID)
}
TTL Configuration
Define TTL as a range at the package level to prevent cache stampede (multiple entries expiring simultaneously):
const ( cacheKeyPrefix = "entity_name:" cacheTTLMin = 12 * time.Hour // Minimum TTL cacheTTLMax = 24 * time.Hour // Maximum TTL )
Use a helper function to calculate randomized TTL:
import (
"math/rand"
"time"
)
func (c *EntityCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
Common TTL ranges:
- •Short-lived:
4-6 minutes- Rate limits, OTP codes - •Session data:
50-70 minutes- User sessions - •Daily data:
12-25 hours- User activation status, daily metrics - •Weekly data:
6.5-7.5 days- Weekly aggregations
Why randomized TTL? When many cache entries are created at the same time (e.g., during traffic spikes), they would all expire simultaneously, causing a "thundering herd" to the database. Randomizing TTL spreads out expirations over time.
Error Handling
Missing Key vs Error
Distinguish between "key not found" (normal) and actual errors:
result := client.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil // Key doesn't exist - not an error
}
return false, err // Actual error
}
Context Usage
Use context.Background() for cache operations unless you have a specific context:
ctx := context.Background()
For operations called from handlers/use cases, accept context as parameter:
func (c *EntityCache) Set(ctx context.Context, id uint64) error {
key := c.buildKey(id)
// Use provided ctx
return c.redisClient.Set(ctx, key, "1", cacheTTL).Err()
}
Naming
- •Port interface:
XxxCache(inportspackage, no suffix) - •Implementation struct:
XxxCache(incachepackage, same name — disambiguated by package) - •Constructor:
NewXxxCache, returns a pointer of the struct implementation - •Constants:
cacheKeyPrefixandcacheTTL(lowercase, package-level)
Fx Wiring
Add to internal/modules/<module>/module.go:
fx.Provide( fx.Annotate( cache.NewXxxCache, fx.As(new(ports.XxxCache)), ), ),
Dependencies
Caches depend on:
- •
redis.UniversalClientfrom"github.com/cristiano-pacheco/bricks/pkg/redis"— Redis operations interface
Example 1: Boolean Flag Cache (User Activation)
Port interface (ports/user_activated_cache.go):
package ports
type UserActivatedCache interface {
Set(userID uint64) error
Get(userID uint64) (bool, error)
Delete(userID uint64) error
}
Implementation (cache/user_activated_cache.go):
package cache
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
)
const (
cacheKeyPrefix = "user_activated:"
cacheTTLMin = 23 * time.Hour
cacheTTLMax = 25 * time.Hour
)
type UserActivatedCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserActivatedCache = (*UserActivatedCache)(nil)
func NewUserActivatedCache(redisClient redis.UniversalClient) *UserActivatedCache {
return &UserActivatedCache{
redisClient: redisClient,
}
}
func (c *UserActivatedCache) Set(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, "1", ttl).Err()
}
func (c *UserActivatedCache) calculateTTL() time.Duration {
min := cacheTTLMin.Milliseconds()
max := cacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserActivatedCache) Get(userID uint64) (bool, error) {
key := c.buildKey(userID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return false, nil
}
return false, err
}
return true, nil
}
func (c *UserActivatedCache) Delete(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserActivatedCache) buildKey(userID uint64) string {
return fmt.Sprintf("%s%s", cacheKeyPrefix, strconv.FormatUint(userID, 10))
}
Fx wiring (module.go):
fx.Provide( fx.Annotate( cache.NewUserActivatedCache, fx.As(new(ports.UserActivatedCache)), ), ),
Example 2: JSON Data Cache (User Session)
DTO (dto/user_session_dto.go):
package dto
import "time"
type UserSessionData struct {
UserID uint64 `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"roles"`
LastActivity time.Time `json:"last_activity"`
IPAddress string `json:"ip_address"`
}
Port interface (ports/user_session_cache.go):
package ports
import (
"time"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/dto"
)
type UserSessionCache interface {
Set(sessionID string, data dto.UserSessionData) error
Get(sessionID string) (*dto.UserSessionData, error)
Delete(sessionID string) error
Exists(sessionID string) (bool, error)
}
Implementation (cache/user_session_cache.go):
package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/dto"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
)
const (
sessionCacheKeyPrefix = "user_session:"
sessionCacheTTLMin = 50 * time.Minute
sessionCacheTTLMax = 70 * time.Minute
)
type UserSessionCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserSessionCache = (*UserSessionCache)(nil)
func NewUserSessionCache(redisClient redis.UniversalClient) *UserSessionCache {
return &UserSessionCache{
redisClient: redisClient,
}
}
func (c *UserSessionCache) Set(sessionID string, data dto.UserSessionData) error {
key := c.buildKey(sessionID)
ctx := context.Background()
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal session data: %w", err)
}
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, jsonData, ttl).Err()
}
func (c *UserSessionCache) calculateTTL() time.Duration {
min := sessionCacheTTLMin.Milliseconds()
max := sessionCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserSessionCache) Get(sessionID string) (*dto.UserSessionData, error) {
key := c.buildKey(sessionID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return nil, nil
}
return nil, err
}
jsonData, err := result.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to get bytes: %w", err)
}
var data dto.UserSessionData
if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, fmt.Errorf("failed to unmarshal session data: %w", err)
}
return &data, nil
}
func (c *UserSessionCache) Delete(sessionID string) error {
key := c.buildKey(sessionID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserSessionCache) Exists(sessionID string) (bool, error) {
key := c.buildKey(sessionID)
ctx := context.Background()
result := c.redisClient.Exists(ctx, key)
if err := result.Err(); err != nil {
return false, err
}
return result.Val() > 0, nil
}
func (c *UserSessionCache) buildKey(sessionID string) string {
return fmt.Sprintf("%s%s", sessionCacheKeyPrefix, sessionID)
}
Fx wiring (module.go):
fx.Provide( fx.Annotate( cache.NewUserSessionCache, fx.As(new(ports.UserSessionCache)), ), ),
Example 3: Protobuf Data Cache (User Profile)
Proto definition (proto/user_profile.proto):
syntax = "proto3";
package identity;
option go_package = "github.com/cristiano-pacheco/pingo/internal/modules/identity/proto";
message UserProfile {
uint64 user_id = 1;
string email = 2;
string name = 3;
repeated string roles = 4;
int64 last_login = 5;
string avatar_url = 6;
}
Port interface (ports/user_profile_cache.go):
package ports
import (
"time"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/proto"
)
type UserProfileCache interface {
Set(userID uint64, profile *proto.UserProfile) error
Get(userID uint64) (*proto.UserProfile, error)
Delete(userID uint64) error
}
Implementation (cache/user_profile_cache.go):
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/cristiano-pacheco/bricks/pkg/redis"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/ports"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/proto"
"google.golang.org/protobuf/proto"
)
const (
profileCacheKeyPrefix = "user_profile:"
profileCacheTTLMin = 12 * time.Hour
profileCacheTTLMax = 24 * time.Hour
)
type UserProfileCache struct {
redisClient redis.UniversalClient
}
var _ ports.UserProfileCache = (*UserProfileCache)(nil)
func NewUserProfileCache(redisClient redis.UniversalClient) *UserProfileCache {
return &UserProfileCache{
redisClient: redisClient,
}
}
func (c *UserProfileCache) Set(userID uint64, profile *proto.UserProfile) error {
key := c.buildKey(userID)
ctx := context.Background()
data, err := proto.Marshal(profile)
if err != nil {
return fmt.Errorf("failed to marshal profile: %w", err)
}
ttl := c.calculateTTL()
return c.redisClient.Set(ctx, key, data, ttl).Err()
}
func (c *UserProfileCache) calculateTTL() time.Duration {
min := profileCacheTTLMin.Milliseconds()
max := profileCacheTTLMax.Milliseconds()
randomMs := min + rand.Int63n(max-min+1)
return time.Duration(randomMs) * time.Millisecond
}
func (c *UserProfileCache) Get(userID uint64) (*proto.UserProfile, error) {
key := c.buildKey(userID)
ctx := context.Background()
result := c.redisClient.Get(ctx, key)
if err := result.Err(); err != nil {
if errors.Is(err, redisClient.Nil) {
return nil, nil
}
return nil, err
}
data, err := result.Bytes()
if err != nil {
return nil, fmt.Errorf("failed to get bytes: %w", err)
}
var profile proto.UserProfile
if err := proto.Unmarshal(data, &profile); err != nil {
return nil, fmt.Errorf("failed to unmarshal profile: %w", err)
}
return &profile, nil
}
func (c *UserProfileCache) Delete(userID uint64) error {
key := c.buildKey(userID)
ctx := context.Background()
return c.redisClient.Del(ctx, key).Err()
}
func (c *UserProfileCache) buildKey(userID uint64) string {
return fmt.Sprintf("%s%d", profileCacheKeyPrefix, userID)
}
Fx wiring (module.go):
fx.Provide( fx.Annotate( cache.NewUserProfileCache, fx.As(new(ports.UserProfileCache)), ), ),
Critical Rules
- •Two files: Port interface in
ports/, implementation incache/ - •Interface in ports: Interface lives in
ports/<name>_cache.go - •Interface assertion: Add
var _ ports.XxxCache = (*XxxCache)(nil)below the struct - •Constructor: MUST return pointer
*XxxCache - •Constants: Define
cacheKeyPrefix,cacheTTLMin, andcacheTTLMaxat package level - •Randomized TTL: MUST use
calculateTTL()helper to prevent cache stampede - •Key builder: Always use a
buildKey()helper method - •Missing keys: Return zero value + nil error, not an error (use
errors.Is(err, redisClient.Nil)) - •Context: Use
context.Background()or acceptcontext.Contextparameter - •No comments: Do not add redundant comments above methods
- •Add detailed comment on interfaces: Provide comprehensive comments on the port interfaces to describe their purpose and usage
- •Redis client type: Use
redis.UniversalClientinterface - •No TTL parameters: TTL is internal to cache, never exposed in interface methods
Workflow
- •Create port interface in
ports/<name>_cache.go - •Create cache implementation in
cache/<name>_cache.go - •Add Fx wiring to module's
module.go - •Run
make lintto verify - •Run
make nilawayfor static analysis