How to Implement Multi-Level Caching with the Tool Struct in Gorig

The Tool struct in Gorig provides a generic, multi-level caching coordinator that implements read-through, write-through, and stampede protection across multiple cache layers using automatic promotion and singleflight synchronization.

Multi-level caching is essential for high-performance Go applications that need to balance speed and persistence. The jom-io/gorig framework offers a powerful solution through its generic Tool struct, which orchestrates multiple cache backends—from in-memory stores to Redis and SQLite—into a unified, hierarchical caching layer.

Understanding the Tool Struct in Gorig

The Tool struct serves as the central coordinator for multi-level caching operations. Defined in cache/cache.go at lines 82-89, it is implemented as a generic type Tool[T any], allowing you to cache any Go data type that supports JSON marshalling.

The struct maintains a slice of cache implementations ordered by speed and durability. When you perform operations like Get or Set, the Tool automatically coordinates across all layers, handling synchronization, promotion, and fallback logic without requiring manual intervention.

Key Features of Multi-Level Caching in Gorig

Read-Through Caching with Loader Functions

The Tool struct implements a read-through pattern via the Get method. When you call Get(key), it traverses the cache layers in order, returning immediately when it finds a hit. If all layers miss, it executes the optional loader function provided during initialization, stores the result in all cache layers, and returns the value.

Write-Through Synchronization

The Set method implements write-through behavior, ensuring consistency across all cache levels. When you call Set(key, value, ttl), the Tool writes the supplied value to every configured cache layer simultaneously. This guarantees that faster in-process caches remain synchronized with slower persistent stores like Redis or SQLite.

Automatic Cache Promotion

When a cache hit occurs in a lower layer (e.g., Redis or JSON file), the Tool automatically promotes that value to all higher layers. The implementation uses an inner loop (for j := 0; j < i; j++) to write the retrieved value back to faster caches above the current hit level, ensuring hot data migrates toward the fastest storage.

Cache Stampede Protection

The Tool struct prevents cache stampedes using a singleflight.Group (referenced as c.group in the source). This mechanism ensures that concurrent requests for the same missing key trigger the loader function only once. All waiting goroutines receive the same result, eliminating thundering herd problems during cache misses.

Thread-Safe Operations

While the built-in cache implementations handle their own synchronization, the Tool struct includes a sync.Mutex (c.mu) to protect mutable state during extensions or custom implementations. This ensures safe concurrent access across all multi-level caching operations.

Implementing Multi-Level Caching: Step-by-Step Guide

Follow these steps to implement multi-level caching in your Gorig application:

  1. Create concrete cache instances using the built-in cache types (Memory, Redis, JSON, Sqlite). Each supports generic type parameters.

  2. Order cache layers from fastest to most durable when constructing the slice. Typical ordering places in-memory caches first, followed by Redis, then persistent file or SQLite stores.

  3. Provide a loader function that knows how to fetch values from your primary data source (database, API, etc.) when all cache layers miss.

  4. Instantiate the Tool using NewCacheTool[T] with your context, ordered cache slice, and loader function.

  5. Use the caching methods (Get, Set, Delete) in your application code. The Tool handles coordination across all layers automatically.

Complete Code Example

The following example demonstrates a complete implementation using memory, Redis, and JSON file caches with an integer type:

package main

import (
	"context"
	"time"

	"github.com/jom-io/gorig/cache"
)

// ---------- 1. Build cache layers ----------
func buildCaches() []cache.Cache[int] {
	// Fast in‑process cache (default expiration 2 min, cleanup every 2 min)
	mem := cache.New[int](cache.Memory, 2*time.Minute, 2*time.Minute)

	// Distributed Redis cache (uses default connection from env vars)
	redis := cache.New[int](cache.Redis)

	// Persistent JSON file cache (file name derived from type name)
	json := cache.New[int](cache.JSON, "int_cache.json")

	return []cache.Cache[int]{mem, redis, json}
}

// ---------- 2. Loader that fetches from the real source ----------
func dbLoader(key string) (int, error) {
	// Imagine this queries a DB; we just return a deterministic value for demo
	// In real code replace with actual DB call.
	return len(key) * 42, nil
}

// ---------- 3. Create the Tool ----------
func newTool() *cache.Tool[int] {
	caches := buildCaches()
	loader := dbLoader
	return cache.NewCacheTool[int](context.Background(), caches, loader)
}

// ---------- 4. Use the Tool ----------
func main() {
	tool := newTool()

	// Read‑through – will hit the loader on first call, then be cached.
	val, err := tool.Get("user:123", 5*time.Minute)
	if err != nil {
		panic(err)
	}
	println("Value:", val)

	// Write‑through – manually update all layers.
	if err := tool.Set("user:123", 999, 5*time.Minute); err != nil {
		panic(err)
	}

	// Delete from every layer.
	if err := tool.Delete("user:123"); err != nil {
		panic(err)
	}
}

Key implementation details in this example:

  • buildCaches demonstrates how to compose multiple cache back‑ends; the order matters for performance.
  • dbLoader represents the loader function; it is only invoked when all layers miss.
  • tool.Get demonstrates read‑through with automatic promotion and singleflight protection without requiring additional code.

Core Source Files

Understanding these source files helps when extending or debugging multi-level caching behavior:

File Purpose
cache/cache.go Core generic cache interfaces, concrete cache factories, and Tool implementation (lines 82‑89)
cache/simple.go In‑memory cache implementation (GoCache) used for the fastest layer
cache/cache.redis.go Redis‑backed cache implementation for distributed scenarios
cache/cache.json.go JSON‑file based persistent cache for local development
cache/cache.sqlite.go SQLite‑backed cache for durable local storage

These files illustrate how Gorig’s caching subsystem can be extended or swapped out, while the Tool struct remains unchanged, providing a clean, generic entry point for multi‑level caching throughout the application.

Summary

  • The Tool[T any] struct in cache/cache.go provides a generic coordinator for multi-level caching strategies.
  • Read-through and write-through patterns are built-in, ensuring data consistency across all cache layers without manual synchronization.
  • Automatic promotion moves hot data from slower persistent stores to faster in-memory caches when hits occur in lower layers.
  • Singleflight protection prevents cache stampedes by ensuring the loader function executes only once for concurrent requests of the same key.
  • The generic design allows caching of any JSON-serializable type while supporting diverse backends including memory, Redis, JSON files, and SQLite.

Frequently Asked Questions

How does the Tool struct prevent cache stampedes in Gorig?

The Tool struct prevents cache stampedes using a singleflight.Group (referenced as c.group in the source code). When multiple goroutines simultaneously request a key that is not present in any cache layer, the singleflight mechanism ensures that the loader function executes only once. All waiting goroutines receive the same result, eliminating the thundering herd problem that typically occurs when cache misses trigger expensive database queries.

What is the correct order for cache layers when using the Tool struct?

You should order cache layers from fastest to most durable when passing the slice to NewCacheTool. The typical arrangement places in-memory caches first (fastest access), followed by distributed caches like Redis, and finally persistent stores such as JSON files or SQLite. This ordering matters because the Tool struct checks caches sequentially during Get operations and automatically promotes data from lower layers to all higher layers when hits occur.

Can I use different data types with the same Tool instance?

No, the Tool struct is generic (Tool[T any]) and type-safe, meaning a single instance works with only one specific data type T. If you need to cache integers and strings in your application, you must create separate Tool[int] and Tool[string] instances. Each instance can still use the same underlying cache backends (memory, Redis, etc.), but they maintain separate type-safe namespaces for their respective data types.

How does automatic cache promotion work in Gorig's multi-level caching?

Automatic promotion occurs during Get operations when a cache hit happens in a lower layer (e.g., Redis or SQLite) but not in the faster layers above it. The Tool struct executes an inner loop (for j := 0; j < i; j++) that writes the retrieved value back to all higher cache layers, effectively "promoting" hot data toward the fastest storage. This happens transparently without requiring additional application code, ensuring that frequently accessed data naturally migrates to the most performant cache layer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →