Skip to content

feat: Add Redis cache adapter#222

Open
vazarkevych wants to merge 3 commits into
mainfrom
feat/add-redis-cache-adapter
Open

feat: Add Redis cache adapter#222
vazarkevych wants to merge 3 commits into
mainfrom
feat/add-redis-cache-adapter

Conversation

@vazarkevych

Copy link
Copy Markdown
Collaborator

Add Redis cache adapter (growthbook-cache-redis)

Summary

Adds a new optional Gradle module, growthbook-cache-redis, that provides a Redis-backed implementation of the SDK's GbCacheManager. This enables sharing the GrowthBook feature cache across multiple instances/pods instead of keeping it in-process or on the local filesystem.

Two Redis clients are supported out of the box:

  • JedisJedisGbCacheManager
  • LettuceLettuceGbCacheManager

Both client dependencies are compileOnly, so consumers include only the client they actually use; nothing is forced onto the SDK's runtime classpath.


Design

Shared base, thin adapters

AbstractRedisGbCacheManager contains the shared logic:

  • key namespacing
  • Redis hash ↔ entry mapping
  • cache-miss vs failure handling
  • FeatureCacheException wrapping

Each adapter only implements low-level Redis operations:

  • writeHash
  • readHash
  • deleteByPrefix

Storage format

Each cache entry is stored as a Redis hash with:

  • data
  • updatedAt (epoch millis)

RedisCacheEntry centralizes field names and mapping so both adapters use the same on-the-wire format.

getLastUpdatedMillis() is backed by updatedAt, enabling freshness-based cache refresh skipping in the SDK.


Configuration (self-typed builder)

AbstractRedisCacheOptions provides configuration:

  • keyPrefix
    Namespace for all keys (default: growthbook:features:).
    clearCache() removes only keys under this prefix via SCAN + DEL (no blocking KEYS).

  • ttl
    Optional per-entry TTL applied on write. null means no expiration.

  • clock
    Injectable clock for deterministic testing.


Connection ownership

The caller owns the lifecycle of:

  • JedisPool
  • StatefulRedisConnection

The adapter only borrows connections and executes commands.


Error handling

  • Missing key → treated as cache miss (null)
  • Real Redis failures → wrapped in FeatureCacheException with the affected key for context

Usage

Jedis

GbCacheManager cache = JedisGbCacheManager.builder()
        .jedisPool(jedisPool)
        .keyPrefix("growthbook:features:")
        .ttl(Duration.ofMinutes(10))
        .buildManager();

Lettuce

GbCacheManager cache = LettuceGbCacheManager.builder()
        .connection(connection)
        .buildManager();

Testing

Unit tests

  • JedisGbCacheManagerTest
  • LettuceGbCacheManagerTest
  • Implemented with Mockito

Integration tests

  • RedisCacheIntegrationTest
  • Runs against real Redis via Testcontainers
  • Located in a separate integrationTest source set
  • Exposed via opt-in integrationTest Gradle task
  • Not included in check, so standard builds do not require Docker

Build / packaging

  • New module registered in settings.gradle
  • Targets Java 8 (sourceCompatibility / targetCompatibility = 1.8)
  • Publishes sources and Javadoc jars to GitHub Packages
  • Fully consistent with existing lib module

Volodymyr Nazarkevych added 3 commits July 3, 2026 16:26
Adds an optional growthbook-cache-redis Gradle module providing
Redis-backed GbCacheManager implementations:

- AbstractRedisGbCacheManager with shared key-prefixing, TTL and
  last-updated timestamp handling.
- JedisGbCacheManager (Jedis pool) and LettuceGbCacheManager
  (Lettuce connection) adapters; both clients are compileOnly so
  consumers add only the one they use.
- Unit tests with mocked clients plus opt-in Testcontainers
  integration tests (separate integrationTest task, requires Docker).
- getLastUpdatedMillis now reads only the updatedAt hash field (HGET)
  instead of fetching the whole payload with HGETALL, so freshness
  checks no longer transfer the full feature JSON.
- Document that Redis Cluster is not supported (multi-key DEL fails
  with CROSSSLOT, SCAN covers a single node) and suggest a hash-tag
  key prefix as a workaround.
- GBFeaturesRepository.shutdown() no longer clears the cache store:
  persisted features are meant to survive restarts, and with shared
  stores (e.g. Redis) clearing on shutdown would wipe the cache for
  every other SDK instance sharing the namespace.
- Fix stale POM description (adapter supports both Jedis and Lettuce).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant