Building an Annotation-Driven Enterprise Cache Framework for Spring Boot Microservices

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.

Leave a Reply

Your email address will not be published. Required fields are marked *