☕ #cache - Caching Directive (Java)
#cache - Caching Directive (Java)
The #cache
directive provides enterprise-grade caching capabilities for Java applications, enabling high-performance data storage and retrieval with Spring Boot integration and Redis backend support.
Basic Syntax
Basic caching - 5 minute TTL
#cache 5m {
#api /data {
return @fetch_expensive_data()
}
}Cache with custom key
#cache 1h key: @request.user.id {
#api /user-profile {
return @get_user_profile(@request.user.id)
}
}Cache with conditions
#cache 30m if: @request.query.cache != "false" {
#api /search {
return @search_database(@request.query.q)
}
}
Java Implementation
import org.tusklang.java.TuskLang;
import org.tusklang.java.directives.CacheDirective;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;@Controller
public class CachedController {
private final TuskLang tuskLang;
private final CacheDirective cacheDirective;
private final DataService dataService;
public CachedController(TuskLang tuskLang, DataService dataService) {
this.tuskLang = tuskLang;
this.cacheDirective = new CacheDirective();
this.dataService = dataService;
}
// Basic caching with Spring annotations
@GetMapping("/api/data")
@Cacheable(value = "data", key = "#root.methodName", unless = "#result == null")
public ResponseEntity<DataResponse> getData() {
return ResponseEntity.ok(dataService.fetchExpensiveData());
}
// Cache with custom key
@GetMapping("/api/user-profile/{userId}")
@Cacheable(value = "user-profiles", key = "#userId", unless = "#result == null")
public ResponseEntity<UserProfile> getUserProfile(@PathVariable Long userId) {
return ResponseEntity.ok(dataService.getUserProfile(userId));
}
// Cache with conditions
@GetMapping("/api/search")
public ResponseEntity<SearchResults> search(
@RequestParam String q,
@RequestParam(required = false) String cache,
HttpServletRequest request) {
if ("false".equals(cache)) {
// Skip cache
return ResponseEntity.ok(dataService.searchDatabase(q));
}
// Use cache
String cacheKey = "search:" + q;
SearchResults results = cacheDirective.get(cacheKey);
if (results == null) {
results = dataService.searchDatabase(q);
cacheDirective.set(cacheKey, results, Duration.ofMinutes(30));
}
return ResponseEntity.ok(results);
}
}
Cache Configuration
Detailed cache configuration
#cache {
ttl: 3600 # Time to live in seconds
key: @request.path # Cache key
condition: @request.method == "GET" # When to cache
tags: ["api", "data"] # Cache tags for invalidation
} {
#api /endpoint {
@process_request()
}
}Multiple cache strategies
#cache {
strategies: [
{ttl: 300, key: "short-term"},
{ttl: 3600, key: "medium-term"},
{ttl: 86400, key: "long-term"}
]
strategy: @select_strategy(@request.path)
} {
#api /* {
@handle_request()
}
}
Java Cache Configuration
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import java.time.Duration;
import java.util.Map;
import java.util.HashMap;@Component
@ConfigurationProperties(prefix = "tusk.cache")
public class CacheConfig {
private int defaultTtl = 3600;
private String defaultKey = "default";
private Map<String, CacheStrategy> strategies;
private boolean enabled = true;
// Getters and setters
public int getDefaultTtl() { return defaultTtl; }
public void setDefaultTtl(int defaultTtl) { this.defaultTtl = defaultTtl; }
public String getDefaultKey() { return defaultKey; }
public void setDefaultKey(String defaultKey) { this.defaultKey = defaultKey; }
public Map<String, CacheStrategy> getStrategies() { return strategies; }
public void setStrategies(Map<String, CacheStrategy> strategies) { this.strategies = strategies; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public static class CacheStrategy {
private int ttl;
private String key;
private String condition;
private List<String> tags;
// Getters and setters
public int getTtl() { return ttl; }
public void setTtl(int ttl) { this.ttl = ttl; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getCondition() { return condition; }
public void setCondition(String condition) { this.condition = condition; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
}
}
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory,
CacheConfig cacheConfig) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(cacheConfig.getDefaultTtl()))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
// Configure different cache strategies
if (cacheConfig.getStrategies() != null) {
for (Map.Entry<String, CacheConfig.CacheStrategy> entry :
cacheConfig.getStrategies().entrySet()) {
CacheConfig.CacheStrategy strategy = entry.getValue();
RedisCacheConfiguration config = defaultConfig
.entryTtl(Duration.ofSeconds(strategy.getTtl()));
cacheConfigurations.put(entry.getKey(), config);
}
}
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
Cache Key Generation
Dynamic cache keys
#cache 1h key: @generate_cache_key(@request.path, @request.query) {
#api /dynamic-data {
return @fetch_data(@request.query.params)
}
}User-specific caching
#cache 30m key: "user-{@auth.id}-{@request.path}" {
#api /user-data {
return @get_user_specific_data(@auth.id)
}
}Complex key generation
#cache 1h key: @hash(@request.path + @request.query + @auth.role) {
#api /complex-endpoint {
return @process_complex_request()
}
}
Java Cache Key Generation
import org.springframework.stereotype.Service;
import java.security.MessageDigest;
import java.util.stream.Collectors;@Service
public class CacheKeyGenerator {
public String generateKey(String path, Map<String, String> queryParams) {
StringBuilder keyBuilder = new StringBuilder(path);
if (queryParams != null && !queryParams.isEmpty()) {
keyBuilder.append("?");
String queryString = queryParams.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.sorted()
.collect(Collectors.joining("&"));
keyBuilder.append(queryString);
}
return keyBuilder.toString();
}
public String generateUserKey(Long userId, String path) {
return String.format("user-%d-%s", userId, path);
}
public String generateComplexKey(String path, Map<String, String> queryParams, String role) {
StringBuilder input = new StringBuilder(path);
if (queryParams != null) {
queryParams.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> input.append(entry.getKey()).append(entry.getValue()));
}
input.append(role);
return hashString(input.toString());
}
private String hashString(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes("UTF-8"));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("Failed to hash string", e);
}
}
}
@RestController
public class DynamicCacheController {
private final CacheKeyGenerator keyGenerator;
private final CacheService cacheService;
private final DataService dataService;
public DynamicCacheController(CacheKeyGenerator keyGenerator,
CacheService cacheService,
DataService dataService) {
this.keyGenerator = keyGenerator;
this.cacheService = cacheService;
this.dataService = dataService;
}
@GetMapping("/api/dynamic-data")
public ResponseEntity<DataResponse> getDynamicData(
@RequestParam Map<String, String> queryParams,
HttpServletRequest request) {
String path = request.getRequestURI();
String cacheKey = keyGenerator.generateKey(path, queryParams);
DataResponse data = cacheService.get(cacheKey, DataResponse.class);
if (data == null) {
data = dataService.fetchData(queryParams);
cacheService.set(cacheKey, data, Duration.ofHours(1));
}
return ResponseEntity.ok(data);
}
@GetMapping("/api/user-data")
public ResponseEntity<UserDataResponse> getUserData(
@AuthenticationPrincipal User user,
HttpServletRequest request) {
String path = request.getRequestURI();
String cacheKey = keyGenerator.generateUserKey(user.getId(), path);
UserDataResponse data = cacheService.get(cacheKey, UserDataResponse.class);
if (data == null) {
data = dataService.getUserSpecificData(user.getId());
cacheService.set(cacheKey, data, Duration.ofMinutes(30));
}
return ResponseEntity.ok(data);
}
}
Cache Invalidation
Automatic cache invalidation
#cache 1h tags: ["users", "profiles"] {
#api /user-profile {
return @get_user_profile(@request.user.id)
}
}Manual invalidation
#cache_evict tags: ["users"] {
#api /update-user {
@update_user_data(@request.user.id)
@invalidate_cache("users")
}
}Conditional invalidation
#cache_evict if: @request.method == "POST" tags: ["data"] {
#api /data {
@update_data()
}
}
Java Cache Invalidation
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;@Service
public class CacheInvalidationService {
private final CacheService cacheService;
public CacheInvalidationService(CacheService cacheService) {
this.cacheService = cacheService;
}
// Automatic cache eviction with Spring annotations
@CacheEvict(value = "user-profiles", key = "#userId")
public void updateUserProfile(Long userId, UserProfile profile) {
// Update user profile
userRepository.save(profile);
}
// Evict by tags
@CacheEvict(value = "users", allEntries = true)
public void invalidateAllUserCaches() {
// This will evict all entries in the "users" cache
}
// Conditional eviction
@CacheEvict(value = "data", condition = "#result != null")
public DataResponse updateData(DataRequest request) {
DataResponse response = dataService.updateData(request);
return response;
}
// Manual cache invalidation
public void invalidateByTag(String tag) {
cacheService.evictByTag(tag);
}
public void invalidateByPattern(String pattern) {
cacheService.evictByPattern(pattern);
}
}
@RestController
public class CacheInvalidationController {
private final CacheInvalidationService invalidationService;
private final UserService userService;
public CacheInvalidationController(CacheInvalidationService invalidationService,
UserService userService) {
this.invalidationService = invalidationService;
this.userService = userService;
}
@PostMapping("/api/update-user")
public ResponseEntity<UserProfile> updateUser(
@RequestBody UserProfile profile,
@AuthenticationPrincipal User user) {
// This will automatically evict the cache
invalidationService.updateUserProfile(user.getId(), profile);
return ResponseEntity.ok(profile);
}
@PostMapping("/api/invalidate-cache")
public ResponseEntity<String> invalidateCache(@RequestParam String tag) {
invalidationService.invalidateByTag(tag);
return ResponseEntity.ok("Cache invalidated");
}
}
Cache Strategies
Write-through caching
#cache 1h strategy: "write-through" {
#api /data {
data: @fetch_data()
@cache_write("data", data)
return data
}
}Write-behind caching
#cache 1h strategy: "write-behind" {
#api /data {
data: @fetch_data()
@cache_queue_write("data", data)
return data
}
}Cache-aside pattern
#cache 1h strategy: "cache-aside" {
#api /data {
cached: @cache_get("data")
if (cached) return cached
data: @fetch_data()
@cache_set("data", data)
return data
}
}
Java Cache Strategies
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;@Service
public class CacheStrategyService {
private final CacheService cacheService;
private final DataService dataService;
private final AsyncTaskExecutor taskExecutor;
public CacheStrategyService(CacheService cacheService,
DataService dataService,
AsyncTaskExecutor taskExecutor) {
this.cacheService = cacheService;
this.dataService = dataService;
this.taskExecutor = taskExecutor;
}
// Write-through strategy
@CachePut(value = "data", key = "#data.id")
public DataResponse writeThrough(DataRequest request) {
DataResponse data = dataService.fetchData(request);
// Cache is automatically updated
return data;
}
// Write-behind strategy
public DataResponse writeBehind(DataRequest request) {
DataResponse data = dataService.fetchData(request);
// Queue the write operation
CompletableFuture.runAsync(() -> {
cacheService.set("data:" + data.getId(), data, Duration.ofHours(1));
}, taskExecutor);
return data;
}
// Cache-aside pattern
public DataResponse cacheAside(String dataId) {
// Try to get from cache first
DataResponse cached = cacheService.get("data:" + dataId, DataResponse.class);
if (cached != null) {
return cached;
}
// Fetch from database
DataResponse data = dataService.fetchDataById(dataId);
// Store in cache
cacheService.set("data:" + dataId, data, Duration.ofHours(1));
return data;
}
// Read-through strategy
@Cacheable(value = "data", key = "#dataId", unless = "#result == null")
public DataResponse readThrough(String dataId) {
return dataService.fetchDataById(dataId);
}
}
Distributed Caching
Distributed cache configuration
#cache {
ttl: 3600
distributed: true
nodes: ["redis-1:6379", "redis-2:6379", "redis-3:6379"]
strategy: "consistent-hashing"
} {
#api /distributed-data {
return @fetch_distributed_data()
}
}Cache replication
#cache {
ttl: 3600
replication: true
master: "redis-master:6379"
slaves: ["redis-slave-1:6379", "redis-slave-2:6379"]
} {
#api /replicated-data {
return @fetch_replicated_data()
}
}
Java Distributed Caching
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class DistributedCacheConfiguration {
@Bean
public RedisConnectionFactory redisClusterConnectionFactory() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
clusterConfig.clusterNode("redis-1", 6379);
clusterConfig.clusterNode("redis-2", 6379);
clusterConfig.clusterNode("redis-3", 6379);
return new LettuceConnectionFactory(clusterConfig);
}
@Bean
public RedisConnectionFactory redisSentinelConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("redis-sentinel-1", 26379)
.sentinel("redis-sentinel-2", 26379)
.sentinel("redis-sentinel-3", 26379);
return new LettuceConnectionFactory(sentinelConfig);
}
@Bean
public CacheManager distributedCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
@Service
public class DistributedCacheService {
private final RedisTemplate<String, Object> redisTemplate;
public DistributedCacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public <T> T get(String key, Class<T> type) {
return (T) redisTemplate.opsForValue().get(key);
}
public void set(String key, Object value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
public void delete(String key) {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
}
Cache Performance Monitoring
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;@Service
public class MonitoredCacheService {
private final CacheService cacheService;
private final Counter cacheHits;
private final Counter cacheMisses;
private final Timer cacheGetTimer;
private final Timer cacheSetTimer;
public MonitoredCacheService(CacheService cacheService, MeterRegistry meterRegistry) {
this.cacheService = cacheService;
this.cacheHits = Counter.builder("cache.hits")
.description("Number of cache hits")
.register(meterRegistry);
this.cacheMisses = Counter.builder("cache.misses")
.description("Number of cache misses")
.register(meterRegistry);
this.cacheGetTimer = Timer.builder("cache.get.duration")
.description("Time taken to get from cache")
.register(meterRegistry);
this.cacheSetTimer = Timer.builder("cache.set.duration")
.description("Time taken to set in cache")
.register(meterRegistry);
}
public <T> T getWithMetrics(String key, Class<T> type) {
return cacheGetTimer.record(() -> {
T value = cacheService.get(key, type);
if (value != null) {
cacheHits.increment();
} else {
cacheMisses.increment();
}
return value;
});
}
public void setWithMetrics(String key, Object value, Duration ttl) {
cacheSetTimer.record(() -> {
cacheService.set(key, value, ttl);
});
}
public double getHitRate() {
double total = cacheHits.count() + cacheMisses.count();
return total > 0 ? cacheHits.count() / total : 0.0;
}
}
Cache Testing
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;@SpringBootTest
@TestPropertySource(properties = {
"spring.cache.type=simple",
"tusk.cache.default-ttl=60"
})
public class CacheTest {
@Autowired
private CacheService cacheService;
@MockBean
private DataService dataService;
@Test
public void testBasicCaching() {
String key = "test-key";
String value = "test-value";
// Set value
cacheService.set(key, value, Duration.ofMinutes(1));
// Get value
String retrieved = cacheService.get(key, String.class);
assertEquals(value, retrieved);
}
@Test
public void testCacheExpiration() throws InterruptedException {
String key = "expiring-key";
String value = "expiring-value";
// Set with short TTL
cacheService.set(key, value, Duration.ofMillis(100));
// Should be available immediately
assertNotNull(cacheService.get(key, String.class));
// Wait for expiration
Thread.sleep(150);
// Should be null after expiration
assertNull(cacheService.get(key, String.class));
}
@Test
public void testCacheInvalidation() {
String key = "invalidation-key";
String value = "invalidation-value";
// Set value
cacheService.set(key, value, Duration.ofMinutes(1));
assertNotNull(cacheService.get(key, String.class));
// Invalidate
cacheService.delete(key);
assertNull(cacheService.get(key, String.class));
}
}
Configuration Properties
application.yml
tusk:
cache:
enabled: true
default-ttl: 3600
default-key: "default"
strategies:
short-term:
ttl: 300
key: "short"
tags: ["temp"]
medium-term:
ttl: 3600
key: "medium"
tags: ["data"]
long-term:
ttl: 86400
key: "long"
tags: ["persistent"]
redis:
host: localhost
port: 6379
database: 0
timeout: 2000
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1spring:
cache:
type: redis
redis:
time-to-live: 3600000
cache-null-values: false
use-key-prefix: true
key-prefix: "tusk:"
Summary
The #cache
directive in TuskLang provides comprehensive caching capabilities for Java applications. With Spring Boot integration, Redis backend, and advanced caching strategies, you can implement high-performance caching that significantly improves application performance.
Key features include: - Multiple caching strategies: Write-through, write-behind, cache-aside, and read-through - Spring Boot integration: Seamless integration with Spring Boot caching annotations - Redis backend: High-performance, distributed caching - Flexible key generation: Dynamic and complex cache key generation - Cache invalidation: Automatic and manual cache invalidation strategies - Distributed caching: Support for Redis clusters and sentinel configurations - Performance monitoring: Built-in metrics and monitoring capabilities - Testing support: Comprehensive testing utilities
The Java implementation provides enterprise-grade caching that scales with your application while maintaining the simplicity and power of TuskLang's declarative syntax.