Skip to main content
Draconis++ includes a sophisticated caching system that reduces redundant system calls and external API requests.

CacheManager overview

The CacheManager class provides a thread-safe, multi-tier caching system:
#include <Drac++/Utils/CacheManager.hpp>

using namespace draconis::utils::cache;
using namespace draconis::utils::types;

CacheManager cache;

Cache tiers

  1. In-memory cache: Fastest, volatile (lost on exit)
  2. Temporary directory: Persists until reboot or system cleanup
  3. Persistent directory: Stored in user cache directory
The cache automatically checks all tiers in order: in-memory → disk → fetch.

Cache locations

enum class CacheLocation : u8 {
  InMemory,      // Volatile, fastest
  TempDirectory, // Persists until reboot
  Persistent     // User-level cache directory
};

Platform-specific cache directories

PlatformPersistent Cache Directory
Linux/BSD~/.cache/draconis++
macOS~/Library/Caches/draconis++
Windows%LOCALAPPDATA%/draconis++/cache

Cache policies

A CachePolicy defines where and how long to cache data:
struct CachePolicy {
  CacheLocation location = CacheLocation::Persistent;
  Option<seconds> ttl = days(1);  // Time-to-live (default: 1 day)
};

Predefined policies

// In-memory only, no expiration
auto policy = CachePolicy::inMemory();

// Persistent, never expires
auto policy = CachePolicy::neverExpire();

// Temporary directory, no expiration
auto policy = CachePolicy::tempDirectory();

Custom policies

using std::chrono::hours;
using std::chrono::minutes;

// Cache for 2 hours in persistent storage
CachePolicy shortLived{
  .location = CacheLocation::Persistent,
  .ttl = hours(2)
};

// Cache for 30 minutes in temp directory
CachePolicy tempShortLived{
  .location = CacheLocation::TempDirectory,
  .ttl = minutes(30)
};
Use shorter TTLs for frequently changing data (e.g., weather) and longer TTLs for static data (e.g., OS version).

Using the cache

Basic usage with getOrSet

The getOrSet method checks the cache or calls a fetcher function:
auto fetchWeather() -> Result<WeatherData> {
  CacheManager cache;
  
  return cache.getOrSet<WeatherData>(
    "weather_data",
    [&]() -> Result<WeatherData> {
      // This only runs on cache miss
      return fetchFromWeatherAPI();
    }
  );
}

With custom policies

auto fetchWeather() -> Result<WeatherData> {
  CacheManager cache;
  
  // Override default policy
  CachePolicy weatherPolicy{
    .location = CacheLocation::Persistent,
    .ttl = minutes(30)  // Weather updates every 30 minutes
  };
  
  return cache.getOrSet<WeatherData>(
    "weather_data",
    weatherPolicy,
    [&]() -> Result<WeatherData> {
      return fetchFromWeatherAPI();
    }
  );
}

In-memory caching

auto getSystemInfo() -> Result<SystemInfo> {
  CacheManager cache;
  
  // Cache only in memory (fast, volatile)
  return cache.getOrSet<SystemInfo>(
    "system_info",
    CachePolicy::inMemory(),
    [&]() -> Result<SystemInfo> {
      return collectSystemInfo();
    }
  );
}

Global cache policy

Set a default policy for all cache operations:
CacheManager cache;

// Set global default
CachePolicy globalPolicy{
  .location = CacheLocation::Persistent,
  .ttl = hours(6)
};
cache.setGlobalPolicy(globalPolicy);

// This uses the global policy
auto result = cache.getOrSet<Data>("key", fetcher);

// This overrides the global policy
auto result2 = cache.getOrSet<Data>("key", CachePolicy::inMemory(), fetcher);

Cache invalidation

Invalidate specific entry

cache.invalidate("weather_data");

Invalidate all entries

// Clear all caches (in-memory and on-disk)
u8 removedCount = cache.invalidateAll();
std::println("Removed {} cache entries", removedCount);

// With logging
u8 removedCount = cache.invalidateAll(true);
invalidateAll() removes all cache files from both persistent and temporary directories. Use with caution.

Bypassing the cache

Disable caching globally at runtime:
// Disable all caching (useful for --ignore-cache CLI flag)
CacheManager::ignoreCache = true;

// All getOrSet calls now skip the cache
auto result = cache.getOrSet<Data>("key", fetcher);
// This always calls fetcher, never reads/writes cache
This is perfect for implementing a --no-cache or --fresh command-line option.

Thread safety

All cache operations are thread-safe:
CacheManager cache;  // Shared across threads

// Thread 1
std::thread t1([&]() {
  auto data = cache.getOrSet<Data>("key1", fetcher1);
});

// Thread 2
std::thread t2([&]() {
  auto data = cache.getOrSet<Data>("key2", fetcher2);
});
Internal mutex protection ensures safe concurrent access without external synchronization.

Cache serialization

The cache uses BEVE (Binary Encoding for Variant-like Entities) format for efficient serialization:
template <typename T>
struct CacheEntry {
  T data;                      // The cached value
  Option<u64> expires;         // UNIX timestamp (None = no expiry)
};
Your cached types must be serializable with glaze. Most standard types and simple structs work automatically.

Performance characteristics

OperationIn-MemoryTemp DirectoryPersistent
Read~10 ns~1-5 ms~1-5 ms
Write~50 ns~5-20 ms~5-20 ms
PersistenceNoUntil rebootIndefinite

Real-world example

using namespace draconis::utils::cache;
using namespace draconis::utils::types;

class WeatherService {
public:
  auto getWeather(StringView city) -> Result<WeatherData> {
    String cacheKey = std::format("weather_{}", city);
    
    CachePolicy policy{
      .location = CacheLocation::Persistent,
      .ttl = minutes(30)  // Weather data stale after 30 min
    };
    
    return m_cache.getOrSet<WeatherData>(
      cacheKey,
      policy,
      [&]() -> Result<WeatherData> {
        // Only called on cache miss
        return fetchFromAPI(city);
      }
    );
  }
  
  auto clearCache() -> void {
    m_cache.invalidateAll();
  }
  
private:
  CacheManager m_cache;
  
  auto fetchFromAPI(StringView city) -> Result<WeatherData> {
    // Network request to weather API
    // ...
  }
};

Best practices

1

Choose appropriate TTLs

// Static data: long or no TTL
CachePolicy osInfo{
  .ttl = days(7)  // OS version rarely changes
};

// Dynamic data: short TTL
CachePolicy weather{
  .ttl = minutes(15)  // Weather updates frequently
};
2

Use in-memory cache for session data

// Data only needed during current run
cache.getOrSet<SessionData>(
  "session",
  CachePolicy::inMemory(),
  fetcher
);
3

Cache expensive operations

// Network requests
cache.getOrSet<ApiResponse>("api_data", fetchFromNetwork);

// Heavy computations
cache.getOrSet<ComputedResult>("computation", expensiveCompute);

// File I/O
cache.getOrSet<FileData>("file_data", readLargeFile);
4

Provide cache invalidation

class Service {
public:
  auto refresh() -> void {
    m_cache.invalidate("my_key");
  }
  
  auto clearAll() -> void {
    m_cache.invalidateAll();
  }
};

Compile-time caching control

Caching can be disabled at compile time:
#if DRAC_ENABLE_CACHING
  // Caching enabled
#else
  // All cache operations become no-ops
#endif
When caching is disabled, getOrSet always calls the fetcher function directly.
CacheManager implementation is in /home/daytona/workspace/source/include/Drac++/Utils/CacheManager.hpp:45.

Build docs developers (and LLMs) love