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:
Compiled Policies - RBAC rules per tenant (TTL: 60s)
User Hierarchy - Subordinates, ancestors, direct reports (TTL: 300s)
Compiled Schemas - Validation rules (TTL: 60s)
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 Type TTL Reasoning Policy 60s Frequent enough to catch policy updates, long enough for hit rate Hierarchy 300s Organizational structure changes slowly Schema 60s Balance 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
Metric Type Labels Description cache_hits_totalCounter typeCache hits by type (policy, hierarchy, schema) cache_misses_totalCounter typeCache misses by type cache_operation_duration_secondsHistogram operationLatency 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
Set appropriate TTLs based on update frequency
Frequently updated : 30-60s (policies, schemas)
Stable data : 5-10 minutes (user hierarchy)
Static data : 30 minutes or more (global config)
Use version-based validation for critical data
Store version separately to avoid deserializing large cached objects just for validation. // Fast path - check version first
if cachedVersion != expectedVersion {
cache . InvalidatePolicy ( ctx , tenantID )
}
Implement cache warming on startup
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 )
}
Invalidate cache on policy/hierarchy updates
Always invalidate affected cache entries when source data changes. // After updating policy
updatePolicyInDB ( tenantID , newPolicy )
cache . InvalidatePolicy ( ctx , tenantID ) // Ensure consistency
Monitor cache hit rates in production
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:
Increase TTL values
Reduce invalidation frequency
Check Redis memory usage and eviction policy
Verify cache keys are constructed correctly
Cache Inconsistency
Symptoms: Stale data served to users
Solutions:
Ensure invalidation is called after DB updates
Reduce TTL for critical data
Implement version-based validation
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