MyTwoCents
MyTwoCents

Reputation: 7622

Spring Cache - Clear cache only when API response is success

I am using Spring Cache @CacheEvict & @Cacheable

Currently I am running a scheduler every Hr to clear cache and next time when fetchUser() is called it will fetch data from external APi and add to cache.

@Scheduled(cron = "0 0 * * * *}")
@CacheEvict(value = "some-unique-value", allEntries = true)
public void clearUserCache() {
    log.info("Cache cleared");
}

@Cacheable(value = "some-unique-value", unless = "#result.isFailure()")
@Override
public Result<UserResponse> fetchUser() {
    try {
        UserResponse userResponse = api.fetchUserDetail();
        return Result.success(userResponse);
    } catch (Exception e) {
        return Result.failure(INTERNAL_SERVER_ERROR);
    }
}

Now what we need is to clear cache only when User API call is success. Is there a way to do that.

As now cache is cleared on schedule and suppose external API call fails. Main API will return error response. In that case I should be able to use existing cache itself.

Upvotes: 2

Views: 2731

Answers (2)

MyTwoCents
MyTwoCents

Reputation: 7622

I didn't find any direct implementation but with a work around I was able to do it.

Use Case

  • User API response should be updated only when next service call is triggered which make use of User API. It should not be updated by scheduler. As we need to pass on header information coming in from external system, to User API as well.
  • Cache must be cleared only when User API response is success.

Steps:

  • Added a variable in scheduler and turning it ON on Schedule time and OFF when cache is updated.
  • This flag is used in UserService class to check if scheduler was triggered or not.
  • If not, use cache. If true, trigger User API call. Check for response, if success. Trigger CacheEvict method and update Cache.

Sample Code:

SchedulerConfig

private boolean updateUserCache;

@Scheduled(cron = "${0 0 * * * *}") // runs every Hr
public void userScheduler() {
    updateUserCache = true;
    log.info("Scheduler triggered for User");
}

@CacheEvict(value = "USER_CACHE", allEntries = true)
public void clearUserCache() {
    updateUserCache = false;
    log.info("User cache cleared");
}

public boolean isUserCacheUpdateRequired() {
    return updateUserCache;
}

UserService

UserResponse userResponse = null;
if (schedulerConfig.isUserCacheUpdateRequired()) {
    userResponse = userCache.fetchUserDetail(); 
    if (userResponse != null) {
        // clear's cache and userResponse is stored in cache automatically when getUserDetail is called below
        schedulerConfig.clearUserCache(); 
    }
}
return userCache.getUserDetail(userResponse);

UserCache

@Cacheable(value = "USER_CACHE", key = "#root.targetClass", unless = "#result.isFailure()")
public Result<User> getUserDetail(UserResponse userResponse) {
    try {
        if (userResponse == null) { // handle first time trigger when cache is not available
            userResponse = fetchUserDetail(); // actual API call 
        }
        return Result.success(mapToUser(userResponse));
    } catch (Exception e) {
        return Result.failure("Error Response");
    }
}

Note:

  • Result is a custom Wrapper, assume it as a object which has success or failure attributes
  • I had to add @Cacheable part as separate Bean because caching only works on proxy objects. If I keep getUserDetail inside UserService and call directly, its not been intercepted as proxy and cache logic is not working, API call is triggered each time.
  • Most important: This is not the best solution and has scope for improvement.

Upvotes: 1

Mario Codes
Mario Codes

Reputation: 717

If I got it correctly, why don't you call it as a normal method after checking the API call is correct at this method's parent?

With your code, something along the lines of

// we just leave scheduled here as you need it.
@Scheduled(cron = "0 0 * * * *}")
@CacheEvict(value = "some-unique-value", allEntries = true)
public void clearUserCache() {
    log.info("Cache cleared");
}

@Cacheable(value = "some-unique-value", unless = "#result.isFailure()")
@Override
public Result<UserResponse> fetchUser() {
    try {
        UserResponse userResponse = api.fetchUserDetail();
        return Result.success(userResponse);
    } catch (Exception e) {
        return Result.failure(INTERNAL_SERVER_ERROR);
    }
}

public void parentMethod() {
    Result<UserResponse> userResult = this.fetchUser();
    if(userResult.isFailure()) {
        this.clearUserCache();
    }
}

This way, if any Exception is thrown it will return with a failure status and you're able to check it. So the cache will be cleared either every hour or when it didn't work.

So the next time, as it was a failure and there's no cache, it will try again.

Upvotes: 1

Related Questions