Skip to main content

Overview

Permission Mongo uses Redis for multi-layer caching to achieve high cache hit rates (>90%) and reduce MongoDB load. The caching layer implements structured key patterns, configurable TTLs, and intelligent invalidation.

Cache Architecture

Cache Layers

┌─────────────────────────────────────────┐
│         Application Layer               │
└──────────────┬──────────────────────────┘

       ┌───────▼────────┐
       │  Redis Cache   │  ← L1: Hot data (TTL: 60-300s)
       └───────┬────────┘
               │ (miss)
       ┌───────▼────────┐
       │    MongoDB     │  ← L2: Persistent storage
       └────────────────┘
Cached Entities:
  1. Compiled Policies - RBAC rules per tenant (TTL: 60s)
  2. User Hierarchy - Subordinates, ancestors, direct reports (TTL: 300s)
  3. Compiled Schemas - Validation rules (TTL: 60s)
  4. AST Cache - Parsed RBAC expressions (in-memory, no TTL)

Cache Key Patterns

All cache keys use the prefix pm: (configurable) followed by structured patterns:
// pkg/cache/redis.go:23-31
const (
    keyPolicyCompiled = "policy:%s:compiled"    // pm:policy:{tenant_id}:compiled
    keyPolicyVersion  = "policy:%s:version"     // pm:policy:{tenant_id}:version
    keyHierSubs       = "hier:%s:%s:subs"       // pm:hier:{tenant_id}:{user_id}:subs
    keyHierDirect     = "hier:%s:%s:direct"     // pm:hier:{tenant_id}:{user_id}:direct
    keyHierAncestors  = "hier:%s:%s:anc"        // pm:hier:{tenant_id}:{user_id}:anc
    keySchemaCompiled = "schema:compiled"       // pm:schema:compiled
)

Key Pattern Examples

pm:policy:acme_corp:compiled      → Compiled policy for tenant "acme_corp"
pm:policy:acme_corp:version       → Policy version for quick validation
pm:hier:acme_corp:user123:subs    → All subordinates of user123
pm:hier:acme_corp:user123:direct  → Direct reports of user123
pm:hier:acme_corp:user123:anc     → Ancestors of user123
pm:schema:compiled                → Global schema definition

TTL Configuration

Default TTL Values

// pkg/cache/redis.go:17-21
const (
    DefaultPolicyTTL    = 60 * time.Second   // Policies change infrequently
    DefaultHierarchyTTL = 300 * time.Second  // Org structure is stable
    DefaultSchemaTTL    = 60 * time.Second   // Schema updates are rare
)
TTL Rationale:
Cache TypeTTLReasoning
Policy60sFrequent enough to catch policy updates, long enough for hit rate
Hierarchy300sOrganizational structure changes slowly
Schema60sBalance between freshness and performance

Custom TTL Configuration

// Override TTLs when setting cache
cache.SetPolicy(ctx, tenantID, policy, 2*time.Minute)
cache.SetSubordinates(ctx, tenantID, userID, subs, 10*time.Minute)

Policy Caching

Caching Compiled Policies

Policies are compiled (RBAC expressions → AST → MongoDB filters) and cached:
// pkg/cache/redis.go:81-87
type CompiledPolicy struct {
    TenantID  string    `json:"tenant_id"`
    Version   string    `json:"version"`
    Data      []byte    `json:"data"`       // Serialized policy
    UpdatedAt time.Time `json:"updated_at"`
}
Write Path:
// Set policy with version tracking
policy := &CompiledPolicy{
    TenantID:  "acme_corp",
    Version:   "v2.1.0",
    Data:      compiledData,
    UpdatedAt: time.Now(),
}

err := cache.SetPolicy(ctx, tenantID, policy, DefaultPolicyTTL)
Read Path:
// Get cached policy
policy, err := cache.GetPolicy(ctx, tenantID)
if err != nil || policy == nil {
    // Cache miss - load from MongoDB
    policy = loadPolicyFromDB(tenantID)
    cache.SetPolicy(ctx, tenantID, policy, DefaultPolicyTTL)
}

Version-Based Invalidation

Policy version is stored separately for fast cache validation:
// Quick version check without deserializing policy
cachedVersion, _ := cache.GetPolicyVersion(ctx, tenantID)
if cachedVersion != currentVersion {
    cache.InvalidatePolicy(ctx, tenantID)
}

Hierarchy Caching

User hierarchy data supports RBAC expressions like user.$subordinates:

Caching Subordinates

// Cache all subordinates (transitive closure)
subs := []string{"user456", "user789", "user012"}
err := cache.SetSubordinates(ctx, tenantID, userID, subs, DefaultHierarchyTTL)

// Retrieve subordinates
subs, err := cache.GetSubordinates(ctx, tenantID, userID)
if subs == nil {
    // Cache miss - compute from DB
    subs = computeSubordinates(tenantID, userID)
    cache.SetSubordinates(ctx, tenantID, userID, subs, DefaultHierarchyTTL)
}

Caching Direct Reports

// Cache direct reports (one level)
reports := []string{"user456", "user789"}
err := cache.SetDirectReports(ctx, tenantID, userID, reports, DefaultHierarchyTTL)

// Retrieve direct reports
reports, err := cache.GetDirectReports(ctx, tenantID, userID)

Caching Ancestors

// Cache ancestor chain (for bottom-up hierarchy navigation)
ancestors := []string{"manager1", "director1", "vp1"}
err := cache.SetAncestors(ctx, tenantID, userID, ancestors, DefaultHierarchyTTL)

Schema Caching

Global schema definitions are cached since they change infrequently:
// pkg/cache/redis.go:89-94
type CompiledSchema struct {
    Version   string    `json:"version"`
    Data      []byte    `json:"data"`       // Serialized schema
    UpdatedAt time.Time `json:"updated_at"`
}

// Cache schema
schema := &CompiledSchema{
    Version:   "v1.0.0",
    Data:      compiledSchema,
    UpdatedAt: time.Now(),
}
err := cache.SetSchema(ctx, schema, DefaultSchemaTTL)

// Retrieve schema
schema, err := cache.GetSchema(ctx)

Cache Invalidation

Policy Invalidation

Invalidate when policies are updated:
// Invalidate specific tenant policy
err := cache.InvalidatePolicy(ctx, tenantID)

// Invalidates both:
// - pm:policy:{tenant_id}:compiled
// - pm:policy:{tenant_id}:version

Hierarchy Invalidation

Granular Invalidation (single user):
// Invalidate hierarchy for one user
err := cache.InvalidateUserHierarchy(ctx, tenantID, userID)

// Invalidates:
// - pm:hier:{tenant_id}:{user_id}:subs
// - pm:hier:{tenant_id}:{user_id}:direct
// - pm:hier:{tenant_id}:{user_id}:anc
Tenant-Wide Invalidation (org restructure):
// Invalidate all hierarchy data for tenant
err := cache.InvalidateHierarchy(ctx, tenantID)

// Scans and deletes all keys matching:
// pm:hier:{tenant_id}:*

Schema Invalidation

// Invalidate global schema cache
err := cache.InvalidateSchema(ctx)

Full Cache Flush

Use with caution in production - impacts all tenants.
// Clear all Permission Mongo cache keys
err := cache.InvalidateAll(ctx)

// Scans and deletes all keys matching: pm:*

Cache Metrics

Permission Mongo exposes Prometheus metrics for cache monitoring:
// pkg/cache/redis.go tracks:
metrics.CacheHits.WithLabelValues("policy").Inc()
metrics.CacheMisses.WithLabelValues("policy").Inc()
metrics.CacheOperationDuration.WithLabelValues("get_policy").Observe(duration)

Key Metrics

MetricTypeLabelsDescription
cache_hits_totalCountertypeCache hits by type (policy, hierarchy, schema)
cache_misses_totalCountertypeCache misses by type
cache_operation_duration_secondsHistogramoperationLatency of cache operations

Monitoring Cache Hit Rate

# Overall cache hit ratio
sum(rate(permission_mongo_cache_hits_total[5m])) /
  (sum(rate(permission_mongo_cache_hits_total[5m])) + 
   sum(rate(permission_mongo_cache_misses_total[5m])))

# Policy cache hit ratio
sum(rate(permission_mongo_cache_hits_total{type="policy"}[5m])) /
  (sum(rate(permission_mongo_cache_hits_total{type="policy"}[5m])) + 
   sum(rate(permission_mongo_cache_misses_total{type="policy"}[5m])))

Cache Statistics

Get runtime cache statistics:
stats, err := cache.Stats(ctx)
// Returns:
// {
//   "connected": true,
//   "pool_stats": "hits=1234 misses=56 timeouts=0 total=100 idle=45 stale=0"
// }

Best Practices

  • Frequently updated: 30-60s (policies, schemas)
  • Stable data: 5-10 minutes (user hierarchy)
  • Static data: 30 minutes or more (global config)
Store version separately to avoid deserializing large cached objects just for validation.
// Fast path - check version first
if cachedVersion != expectedVersion {
    cache.InvalidatePolicy(ctx, tenantID)
}
Pre-populate cache with hot data during application startup.
// Warm cache with active tenants
for _, tenantID := range getActiveTenants() {
    policy := loadPolicy(tenantID)
    cache.SetPolicy(ctx, tenantID, policy, DefaultPolicyTTL)
}
Always invalidate affected cache entries when source data changes.
// After updating policy
updatePolicyInDB(tenantID, newPolicy)
cache.InvalidatePolicy(ctx, tenantID)  // Ensure consistency
Target >90% hit rate for optimal performance. Low hit rates indicate:
  • TTL too short
  • High cache churn (excessive invalidation)
  • Insufficient Redis memory

Troubleshooting

Low Cache Hit Rate

Symptoms: Hit rate less than 80%, high MongoDB load Solutions:
  1. Increase TTL values
  2. Reduce invalidation frequency
  3. Check Redis memory usage and eviction policy
  4. Verify cache keys are constructed correctly

Cache Inconsistency

Symptoms: Stale data served to users Solutions:
  1. Ensure invalidation is called after DB updates
  2. Reduce TTL for critical data
  3. Implement version-based validation
  4. Use cache-aside pattern (check DB if version mismatch)

Redis Connection Errors

Symptoms: connection refused, pool timeout Solutions:
# Increase pool size and timeout
redis:
  pool_size: 1000
  pool_timeout: 5s
  read_timeout: 2s

Next Steps

Performance Tuning

Optimize connection pools and throughput

Expression Language

Learn RBAC expression syntax for policy caching

Build docs developers (and LLMs) love