Caching
The Java SDK supports caching to reduce API calls and improve performance.
Caffeine Cache
Caffeine provides high-performance in-memory caching, ideal for single-instance applications.
Installation
<dependency>
<groupId>io.toggly</groupId>
<artifactId>toggly-cache-caffeine</artifactId>
<version>1.7.0</version>
</dependency>
Configuration
SnapshotProvider httpProvider = new HttpSnapshotProvider(config);
SnapshotProvider cachedProvider = new CaffeineCachingSnapshotProvider(
httpProvider,
CaffeineCacheConfig.builder()
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1))
.maximumSize(100)
.recordStats()
.build()
);
TogglyClient client = new TogglyClient(config, cachedProvider);
The examples assume an existing TogglyConfig config. Keep the client/provider
for the application's lifetime. Because the provider was supplied to the client,
close client first and cachedProvider second at shutdown; the outer cache closes
the HTTP delegate. HTTP polling/live updates do not evict this outer snapshot
cache, so its TTL/refresh policy can delay the definitions visible to evaluation.
Call the outer provider's refresh() to fetch through it explicitly.
Configuration Options
| Option | Default | Description |
|---|---|---|
expireAfterWrite | - | Time after which entries expire |
refreshAfterWrite | - | Time after which entries are refreshed asynchronously |
maximumSize | 0 (unlimited) | Maximum number of entries |
recordStats | false | Enable statistics recording |
Accessing Statistics
CaffeineCachingSnapshotProvider provider = ...;
CacheStats stats = provider.stats();
System.out.println("Hit rate: " + stats.hitRate());
System.out.println("Miss rate: " + stats.missRate());
System.out.println("Load count: " + stats.loadCount());
Evaluation Cache
Definition caching keeps evaluation sensitive to the current request context. Only add a result cache if stale answers are acceptable for its TTL. Its key uses the feature, identity and context hash (including claims/request/entity), but no definition revision or current time. A refresh or TimeWindow boundary therefore does not automatically invalidate an answer.
Use a short TTL and explicitly invalidate from application code when you know
definitions have changed. TogglyClient.onRefresh does not currently invoke
listeners; it cannot wire automatic invalidation. The following cache must be
long-lived, and every evaluation must pass the complete context:
CaffeineEvaluationCache evaluationCache = CaffeineEvaluationCache.builder()
.expireAfterWrite(Duration.ofSeconds(10))
.maximumSize(10000)
.recordStats()
.build();
// Use cached evaluation
boolean enabled = evaluationCache.getOrCompute(
"my-feature",
context,
() -> togglyClient.isEnabled("my-feature", context)
);
// Invalidate when features change
evaluationCache.invalidateFeature("my-feature");
evaluationCache.invalidateAll();
Redis Cache
Redis provides distributed caching, ideal for multi-instance deployments.
Installation
<dependency>
<groupId>io.toggly</groupId>
<artifactId>toggly-cache-redis</artifactId>
<version>1.7.0</version>
</dependency>
Configuration
- Basic
- With Authentication
- With Existing Pool
SnapshotProvider redisProvider = new RedisCachingSnapshotProvider(
new HttpSnapshotProvider(config),
RedisCacheConfig.builder()
.host("localhost")
.port(6379)
.ttl(Duration.ofMinutes(5))
.build()
);
TogglyClient client = new TogglyClient(config, redisProvider);
RedisCacheConfig redisConfig = RedisCacheConfig.builder()
.host("redis.example.com")
.port(6379)
.password("your-password")
.database(0)
.ssl()
.ttl(Duration.ofMinutes(5))
.keyPrefix("myapp:toggly:")
.timeout(2000)
.build();
SnapshotProvider redisProvider = new RedisCachingSnapshotProvider(
new HttpSnapshotProvider(config),
redisConfig
);
// Use an existing JedisPool
JedisPool existingPool = ...;
SnapshotProvider redisProvider = new RedisCachingSnapshotProvider(
new HttpSnapshotProvider(config),
existingPool,
"toggly:", // key prefix
Duration.ofMinutes(5) // TTL
);
Configuration Options
| Option | Default | Description |
|---|---|---|
host | localhost | Redis host |
port | 6379 | Redis port |
password | null | Redis password |
database | 0 | Redis database number |
keyPrefix | toggly: | Prefix for cache keys |
ttl | 5 minutes | Time-to-live for cached entries |
timeout | 2000 | Connection timeout in milliseconds |
ssl | false | Enable SSL/TLS |
Close the client before redisProvider at shutdown. The Redis wrapper closes its
HTTP delegate and any Jedis pool it created itself. An existing pool remains
caller-owned. Do not close a shared pool from each client.
Cache Invalidation
RedisCachingSnapshotProvider provider = ...;
// Invalidate the cache
provider.invalidate();
Spring Boot Integration
Choose one of these configurations in your component-scanned package. It
replaces the plain HTTP provider bean from the Spring setup, not the
client. TogglyConfig is the SDK type; FeatureCacheConfiguration is your class.
Spring closes the outer provider, which closes its HTTP delegate. The Redis tab
requires application properties spring.redis.host and spring.redis.port.
- Caffeine
- Redis
import io.toggly.core.config.TogglyConfig;
import io.toggly.core.snapshot.HttpSnapshotProvider;
import io.toggly.core.snapshot.SnapshotProvider;
import io.toggly.cache.caffeine.CaffeineCacheConfig;
import io.toggly.cache.caffeine.CaffeineCachingSnapshotProvider;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeatureCacheConfiguration {
@Bean(destroyMethod = "close")
public SnapshotProvider snapshotProvider(TogglyConfig config) {
return new CaffeineCachingSnapshotProvider(
new HttpSnapshotProvider(config),
CaffeineCacheConfig.builder()
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1))
.recordStats()
.build()
);
}
}
import io.toggly.core.config.TogglyConfig;
import io.toggly.core.snapshot.HttpSnapshotProvider;
import io.toggly.core.snapshot.SnapshotProvider;
import io.toggly.cache.redis.RedisCacheConfig;
import io.toggly.cache.redis.RedisCachingSnapshotProvider;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeatureCacheConfiguration {
@Bean(destroyMethod = "close")
public SnapshotProvider snapshotProvider(
TogglyConfig config,
@Value("${spring.redis.host}") String redisHost,
@Value("${spring.redis.port}") int redisPort) {
return new RedisCachingSnapshotProvider(
new HttpSnapshotProvider(config),
RedisCacheConfig.builder()
.host(redisHost)
.port(redisPort)
.ttl(Duration.ofMinutes(5))
.build()
);
}
}
Caching Strategies
1. Cache-Aside (Default)
The SDK checks cache first, fetches from API on miss:
// Automatic cache-aside behavior
boolean enabled = client.isEnabled("my-feature");
// 1. Check cache
// 2. If miss, fetch from API
// 3. Store in cache
// 4. Return result
2. Write-Through
Cache is updated when refreshing:
// Fetch definitions and update this local cache (does not edit dashboard flags)
client.refresh();
3. Background Refresh
Caffeine can refresh entries asynchronously:
CaffeineCacheConfig.builder()
.expireAfterWrite(Duration.ofMinutes(10))
.refreshAfterWrite(Duration.ofMinutes(1)) // Async refresh
.build();
Best Practices
- Use appropriate TTL: Balance freshness vs. performance
- Monitor cache stats: Track hit rates to optimize settings
- Use Redis for distributed: When running multiple instances
- Prefer definition caching: Result caches can retain stale targeting decisions
- Handle cache failures gracefully: SDK falls back to API
Reliability (signed snapshots)
When signed definitions are enabled, snapshots store exact server defs JSON
(signedDefsJson) for verification — never re-serialize models to verify.
Configure onError on TogglyConfig.Builder, and call clearCache() to drop
persisted feature/JWKS snapshots after corruption or key rotation.
WebSocket signing-key-updated clears JWKS and forces a definitions refresh
(see Live updates).
Full signed-definitions setup: Signed definitions.
Cache Warming
Attempt to pre-fetch definitions at startup. The callback in your TogglyConfig
reports failures; completion of this listener is not proof of a fresh snapshot:
@Component
public class CacheWarmer implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private TogglyClient togglyClient;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// Attempt to warm the cache
togglyClient.refresh();
}
}