Introduction
In the previous articles, we built a reusable caching library that provides:
- L1 Cache using EhCache
- L2 Cache using Redis or GemFire
- Distributed Locking
- Cache Stampede Protection
- Cache Warming
- Background Refresh
- Kafka Based Invalidation
- Spring Event Support
- Metrics & Monitoring
While the framework is already powerful, developers still need to write code like:
return cacheManager.get(
"product:" + productId,
() -> repository.findById(productId),
300);
Although this centralizes caching logic, developers are still required to interact directly with the cache framework.
A better approach is to make caching completely transparent using annotations and Spring AOP.
Instead of writing cache code, developers simply write:
@PlatformCacheable(
key = "'product:' + #productId",
ttl = 300)
public Product getProduct(
String productId) {
return repository.findById(productId)
.orElse(null);
}
The cache framework automatically handles:
- L1 Lookup
- L2 Lookup
- Distributed Locking
- Cache Population
- Metrics
- Stampede Protection
- Refresh Logic
- Kafka Invalidation
This article extends our platform cache library to become fully annotation driven.
Why Annotation Driven Caching?
Without annotations:
@Service
public class ProductService {
@Autowired
MultiLevelCacheManager cacheManager;
public Product getProduct(
String id) {
return cacheManager.get(
"product:" + id,
() -> repository
.findById(id)
.orElse(null),
300);
}
}
Problems:
- Repetitive code
- Developers forget cache usage
- Business logic mixed with cache logic
With annotations:
@PlatformCacheable(
key="'product:' + #id",
ttl=300)
public Product getProduct(
String id) {
return repository
.findById(id)
.orElse(null);
}
Business logic remains clean.
Final Library Architecture
Controller
|
Service
|
Spring AOP
|
Platform Cache Aspect
|
L1 Cache (EhCache)
|
L2 Cache (Redis/GemFire)
|
Repository
|
Database
Microservices never interact directly with cache APIs.
New Package Structure
platform-cache-lib
com.company.cache
├── annotation
│ ├── PlatformCacheable
│ ├── PlatformCachePut
│ ├── PlatformCacheEvict
│ ├── PlatformCacheWarm
│ └── PlatformCacheRefresh
│
├── aspect
│ ├── CacheableAspect
│ ├── CachePutAspect
│ ├── CacheEvictAspect
│ ├── CacheWarmAspect
│
├── core
│ ├── MultiLevelCacheManager
│
├── lock
│
├── warming
│
├── refresh
│
└── invalidation
Annotation 1: @PlatformCacheable
Equivalent to:
Check L1
↓
Check L2
↓
Acquire Lock
↓
Call Method
↓
Store in Cache
↓
Return Result
Annotation Definition
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformCacheable {
String key();
long ttl() default 300;
}
Usage
@Service
public class ProductService {
@PlatformCacheable(
key="'product:' + #productId",
ttl=300)
public Product getProduct(
String productId) {
return repository
.findById(productId)
.orElse(null);
}
}
Cacheable Aspect
@Aspect
@Component
public class CacheableAspect {
@Autowired
private MultiLevelCacheManager
cacheManager;
@Around(
"@annotation(cacheable)")
public Object cache(
ProceedingJoinPoint pjp,
PlatformCacheable cacheable)
throws Throwable {
String key =
KeyGenerator.generate(
cacheable.key(),
pjp);
return cacheManager.get(
key,
() -> {
try {
return pjp.proceed();
} catch(Throwable e) {
throw new RuntimeException(e);
}
},
cacheable.ttl());
}
}
Developers never write cache code again.
Annotation 2: @PlatformCachePut
Used after create/update operations.
Purpose:
Database Updated
↓
Update L1
↓
Update L2
Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformCachePut {
String key();
long ttl() default 300;
}
Example
@PlatformCachePut(
key="'product:' + #product.id",
ttl=300)
public Product save(
Product product) {
return repository.save(product);
}
Internal Flow
Method Executes
↓
Returns Product
↓
Framework Updates L2
↓
Framework Updates L1
↓
Return Response
Cache Put Aspect
@AfterReturning(
pointcut=
"@annotation(cachePut)",
returning="result")
public void updateCache(
PlatformCachePut cachePut,
Object result) {
String key =
evaluateKey(...);
cacheManager.put(
key,
result,
cachePut.ttl());
}
Annotation 3: @PlatformCacheEvict
Used after delete/update operations.
Purpose:
Database Updated
↓
Invalidate L2
↓
Publish Kafka Event
↓
Invalidate L1 Everywhere
Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformCacheEvict {
String key();
}
Example
@PlatformCacheEvict(
key="'product:' + #id")
public void deleteProduct(
String id) {
repository.deleteById(id);
}
Eviction Flow
Delete Product
↓
Delete Redis/GemFire
↓
Publish Kafka Event
↓
All Pods Receive Event
↓
All L1 Caches Cleared
Annotation 4: @PlatformCacheWarm
Used for startup cache preloading.
Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformCacheWarm {
}
Example
@Component
public class ProductWarmup {
@PlatformCacheWarm
public List<Product>
loadProducts() {
return repository
.findTop100Products();
}
}
Startup Flow
Application Starts
↓
Find @PlatformCacheWarm
↓
Execute Method
↓
Populate L1
↓
Populate L2
Annotation 5: @PlatformCacheRefresh
Used for hot data.
Automatically refreshes cache.
Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformCacheRefresh {
long interval();
}
Example
@PlatformCacheRefresh(
interval=300)
public Product getPopularProduct(
String id) {
return repository
.findById(id)
.orElse(null);
}
Refresh Flow
Cache Near Expiry
↓
Background Thread
↓
Reload Data
↓
Update Cache
↓
No User Impact
Key Generation Engine
Developers should not manually build cache keys.
Example
@PlatformCacheable(
key="'product:' + #id")
Generated:
product:1001
Utility
public class KeyGenerator {
public static String generate(
String expression,
ProceedingJoinPoint pjp) {
return spelParser
.parseExpression(
expression)
.getValue(...)
.toString();
}
}
Microservice Examples
GET Product
@PlatformCacheable(
key="'product:' + #id",
ttl=600)
public Product getProduct(
String id) {
return repository
.findById(id)
.orElse(null);
}
Framework:
L1
↓
L2
↓
DB
↓
Populate Cache
CREATE Product
@PlatformCachePut(
key="'product:' + #result.id")
public Product createProduct(
Product product) {
return repository.save(product);
}
Framework:
Save DB
↓
Update L1
↓
Update L2
UPDATE Product
Option 1
@PlatformCachePut(
key="'product:' + #product.id")
public Product updateProduct(
Product product) {
return repository.save(product);
}
Immediate cache update.
Option 2
@PlatformCacheEvict(
key="'product:' + #product.id")
public Product updateProduct(
Product product) {
return repository.save(product);
}
Cache rebuilt on next read.
DELETE Product
@PlatformCacheEvict(
key="'product:' + #id")
public void deleteProduct(
String id) {
repository.deleteById(id);
}
Framework:
Delete L2
↓
Publish Kafka Event
↓
Delete All L1 Copies
Product Catalog Warmup
@PlatformCacheWarm
public List<Product>
loadFrequentlyUsedProducts() {
return repository
.findTop100Products();
}
Executed automatically during startup.
Metrics Produced Automatically
No developer effort.
cache.hit.l1
cache.hit.l2
cache.miss
cache.put
cache.evict
cache.refresh
cache.warm
cache.lock.acquired
cache.lock.failed
cache.stampede.prevented
Microservice Configuration
cache.enabled=true
cache.l1.enabled=true
cache.l1.maxEntries=10000
cache.l2.type=redis
cache.lock.enabled=true
cache.warming.enabled=true
cache.refresh.enabled=true
cache.refresh.interval=300
cache.ttl.jitter=20
cache.invalidation.type=kafka
What Microservice Teams Need To Do
Add dependency:
<dependency>
<groupId>
com.company.platform
</groupId>
<artifactId>
platform-cache-lib
</artifactId>
</dependency>
Annotate methods:
@PlatformCacheable
@PlatformCachePut
@PlatformCacheEvict
@PlatformCacheWarm
@PlatformCacheRefresh
Done.
No Redis code.
No GemFire code.
No Kafka code.
No lock management.
No invalidation logic.
No cache warming code.
Final Architecture
Controller
|
Service
|
Annotations
|
Spring AOP
|
Platform Cache Framework
|
----------------------------
| |
L1 Cache L2 Cache
EhCache Redis/GemFire
|
Distributed Locking
|
Kafka Invalidation
|
Metrics
|
Database
Conclusion
By introducing annotation-driven caching, the platform cache library becomes a true enterprise caching framework. Developers focus exclusively on business logic while the framework transparently manages multi-level caching, distributed locking, cache warming, refresh strategies, invalidation events, metrics, and cache stampede protection.
The result is a consistent caching approach across all microservices with minimal code, centralized governance, and production-grade scalability.