In-Memory Caching with TTL and Eviction
Thread-safe in-memory cache with TTL expiration, cleanup ticker, and LRU-style eviction when capacity is reached.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
package main
import (
"container/list"
"fmt"
"log"
"sync"
"time"
)
// CacheItem represents a single cache entry with expiration time
type CacheItem struct {
key string
value interface{}
expiration time.Time
listElement *list.Element // For LRU tracking
}
// Cache is a thread-safe in-memory cache with TTL and capacity limits
type Cache struct {
mu sync.RWMutex
items map[string]*CacheItem
lruList *list.List // Tracks access order for LRU eviction
maxCapacity int // Maximum number of items
defaultTTL time.Duration // Default TTL for items
cleanupTicker *time.Ticker // Ticker for periodic expired item cleanup
}
// NewCache creates a new Cache with specified capacity and default TTL
func NewCache(maxCapacity int, defaultTTL time.Duration, cleanupInterval time.Duration) *Cache {
c := &Cache{
items: make(map[string]*CacheItem),
lruList: list.New(),
maxCapacity: maxCapacity,
defaultTTL: defaultTTL,
cleanupTicker: time.NewTicker(cleanupInterval),
}
// Start background cleanup goroutine
go c.startCleanupWorker()
return c
}
// startCleanupWorker periodically removes expired items
func (c *Cache) startCleanupWorker() {
for range c.cleanupTicker.C {
c.cleanupExpired()
}
}
// cleanupExpired removes all expired items from the cache
func (c *Cache) cleanupExpired() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
expiredCount := 0
for key, item := range c.items {
if item.expiration.Before(now) {
c.lruList.Remove(item.listElement)
delete(c.items, key)
expiredCount++
}
}
if expiredCount > 0 {
log.Printf("Cleaned up %d expired cache items", expiredCount)
}
}
// Set adds or updates an item in the cache with optional custom TTL
func (c *Cache) Set(key string, value interface{}, ttl ...time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
// Determine TTL (use custom if provided, else default)
itemTTL := c.defaultTTL
if len(ttl) > 0 {
itemTTL = ttl[0]
}
expiration := time.Now().Add(itemTTL)
// If key exists, update it and move to front of LRU list
if existingItem, ok := c.items[key]; ok {
existingItem.value = value
existingItem.expiration = expiration
c.lruList.MoveToFront(existingItem.listElement)
return
}
// If at max capacity, evict least recently used item
if c.lruList.Len() >= c.maxCapacity {
evicted := c.lruList.Back()
if evicted != nil {
evictedItem := evicted.Value.(*CacheItem)
delete(c.items, evictedItem.key)
c.lruList.Remove(evicted)
log.Printf("Evicted LRU item: %s", evictedItem.key)
}
}
// Create new cache item
item := &CacheItem{
key: key,
value: value,
expiration: expiration,
}
// Add to LRU list and map
item.listElement = c.lruList.PushFront(item)
c.items[key] = item
}
// Get retrieves an item from the cache (returns nil if not found/expired)
func (c *Cache) Get(key string) interface{} {
c.mu.RLock()
item, ok := c.items[key]
c.mu.RUnlock()
// Check if item exists and is not expired
if !ok || item.expiration.Before(time.Now()) {
return nil
}
// Update LRU (move to front)
c.mu.Lock()
c.lruList.MoveToFront(item.listElement)
c.mu.Unlock()
return item.value
}
// Close stops the cleanup ticker and releases resources
func (c *Cache) Close() {
c.cleanupTicker.Stop()
}
// Example usage
func main() {
// Create cache: max 5 items, 10s default TTL, 5s cleanup interval
cache := NewCache(5, 10*time.Second, 5*time.Second)
defer cache.Close()
// Add items to cache
cache.Set("user:1", map[string]interface{}{"id": 1, "name": "Alice"})
cache.Set("user:2", map[string]interface{}{"id": 2, "name": "Bob"})
cache.Set("config:timeout", 30, 30*time.Second) // Custom TTL
// Retrieve items
if user1 := cache.Get("user:1"); user1 != nil {
fmt.Printf("Found user1: %+v\n", user1)
}
if timeout := cache.Get("config:timeout"); timeout != nil {
fmt.Printf("Found timeout config: %d\n", timeout.(int))
}
// Test expiration (short TTL item)
cache.Set("temp:data", "short-lived", 1*time.Second)
time.Sleep(2 * time.Second)
if tempData := cache.Get("temp:data"); tempData == nil {
fmt.Println("temp:data expired as expected")
}
// Test LRU eviction (add 6 items to exceed capacity of 5)
for i := 3; i <= 7; i++ {
cache.Set(fmt.Sprintf("user:%d", i), map[string]interface{}{"id": i, "name": fmt.Sprintf("User %d", i)})
}
// Check if least recently used (user:2) was evicted
if user2 := cache.Get("user:2"); user2 == nil {
fmt.Println("user:2 evicted (LRU) as expected")
}
}How It Works
Thread-safe in-memory cache with TTL expiration, cleanup ticker, and LRU-style eviction when capacity is reached.
Stores items in a map plus a linked list for LRU tracking; Set attaches expiration and moves keys to the front; Get checks expiry and updates recency; a background janitor evicts expired entries until Stop is called.
Key Concepts
- 1Combination of map and list gives O(1) get and set with recency tracking.
- 2TTL per entry and periodic cleanup prevent stale data buildup.
- 3Capacity guard evicts the oldest items when the max size is exceeded.
When to Use This Pattern
- Lightweight caching layer inside services without Redis.
- Short-lived request caches for expensive computations.
- Feature flag or configuration caching with expiration.
Best Practices
- Stop the cleanup goroutine during shutdown to avoid leaks.
- Tune TTL and capacity based on workload and memory budget.
- Handle missing values gracefully to trigger fresh loads.
Go Version1.14
Difficultyadvanced
Production ReadyYes
Lines of Code175