Implementing a Custom Request scope cache Annotation with AOP in Spring Boot

Rahul KRahul K
3 min read

Caching in Spring Boot can go beyond traditional mechanisms like Redis or Guava. What if you could mark methods for request-level caching just by annotating them? Enter a custom @RequestScopedCache annotation, powered by AOP and request-scoped beans.

The Idea

We want to annotate methods so that their results are cached for the duration of a single HTTP request. If the method is called again with the same arguments during the same request, the cached result is returned. I found it quite useful since we have usually a request going through multiple phases and still calling same methods which might be cpu intensive or even network intensive and we cannot hold it in global cache since they often have results based on request data which can potentially be keep coming unique.

Step 1: Create a @RequestScopedCache Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestScopedCache {
}

Step 2: Build a Request-Scoped Cache Holder

@Component
@RequestScope
public class RequestCacheHolder {
    private final Map<String, Object> cache = new HashMap<>();
    public Object get(String key) {
        return cache.get(key);
    }
    public void put(String key, Object value) {
        cache.put(key, value);
    }
    public boolean contains(String key) {
        return cache.containsKey(key);
    }
}

Step 3: Create an Aspect to Intercept Annotated Methods

@Aspect
@Component
public class RequestScopedCacheAspect {

    private final RequestCacheHolder requestCacheHolder;
    public RequestScopedCacheAspect(RequestCacheHolder requestCacheHolder) {
        this.requestCacheHolder = requestCacheHolder;
    }
    @Around("@annotation(RequestScopedCache)")
    public Object cacheAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = generateKey(joinPoint);
        if (requestCacheHolder.contains(key)) {
            return requestCacheHolder.get(key);
        }
        Object result = joinPoint.proceed();
        requestCacheHolder.put(key, result);
        return result;
    }
    private String generateKey(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        return method.getName() + Arrays.toString(args);
    }
}

Note: We intentionally avoid including class names in the cache key to prevent any potential security or exposure concerns.

Step 4: Apply the Annotation on applicable method

@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final CustomizationApiClient customizationApiClient;
    public ProductService(ProductRepository productRepository, CustomizationApiClient customizationApiClient) {
        this.productRepository = productRepository;
        this.customizationApiClient = customizationApiClient;
    }
    @RequestScopedCache
    public Product getCustomizedProduct(String productId, String userPreference) {
        Product product = productRepository.findById(productId)
                                           .orElseThrow(() -> new RuntimeException("Product not found"));
        // Add customization based on user preference using a third-party API
        return customizationApiClient.applyCustomization(product, userPreference);
    }
}

In this example, even if getCustomizedProduct is called multiple times with the same parameters within a single request, the customization logic and database call will only run once.

Benefits

  • Clean and declarative caching

  • Efficient reuse within a request

  • Avoids redundant logic in services

Caveats

  • Works best for idempotent, deterministic methods

  • Limited to request scope, not suitable for session or global caching

Comparison with global in-memory cache

When to Use Which:

  • Use Request Scope Cache when some heavily-processed data based on request argument is needed multiple times within the same request but is too transient to justify global caching.

  • Use Global Cache when the same data benefits multiple users or requests, and freshness can be managed appropriately.

Conclusion

Creating a @RequestScopedCache annotation in Spring Boot is a powerful pattern when you want easy-to-manage, low-overhead caching at the HTTP request level. Combined with AOP, it keeps your service logic clean while boosting performance where it matters most.

#SpringBoot #Java #Caching #RequestScope #AOP #SoftwareArchitecture #BackendDevelopment


Originally published on Medium

0
Subscribe to my newsletter

Read articles from Rahul K directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rahul K
Rahul K

I write about what makes good software great — beyond the features. Exploring performance, accessibility, reliability, and more.