javaspring-cache

Spring caching + how to put result of @Cachable into multiple caches


I have a service that resolve customer identifiers in some domain. I have a simple pojo model of a customer identifiers:

@Data
public class Identifiers {
    private String accountNumber;
    private Long customerId;
}

And i have a service, that resolving (fetching) identifiers by calling multiple slow rest api. For test/demo purposes just imagine something like this:

@Service
@RequiredArgsConstructor
public class LazyIdentifiersService {
    @SneakyThrows
    public Identifiers getIdentifiers() {
        TimeUnit.SECONDS.sleep(3);
        var identifiers = new Identifiers();
        identifiers.setAccountNumber("a");
        identifiers.setCustomerId(1L);
        return identifiers;
    }
}

I want to caching my "result" (Identifiers model for customer) if i resolved that by one of properties. I`m starting with this:

@Service
@RequiredArgsConstructor
public class CachedIdentifiersCache {

    private final LazyIdentifiersService slowServiceWithMultipleRestInvocations;

    @Cacheable(value = {"identifiers_cache_by_customer_id"}, key = "#customerId")
    public Identifiers getCachedIdentifiers(Long customerId) {
        return slowServiceWithMultipleRestInvocations.getIdentifiers();
    }
    @Cacheable(value = {"identifiers_cache_by_account_number"}, key = "#accountNumber")
    public Identifiers getCachedIdentifiers(String accountNumber) {
        return slowServiceWithMultipleRestInvocations.getIdentifiers();
    }
}

Is that possible to save result into both cache from single calling ?

Use case that i want to realize:

  1. Someone calls my service to resolve customer (identifiers) by account Number.
  2. My caches are empty, and i call slow services to fetch and build Identifiers model.
  3. I want to save my result into both caches (identifiers_cache_by_customer_id and identifiers_cache_by_account_number)
  4. Next time someone calls my service to resolve identifiers by customerId, i just want to get it from cache.

Solution

  • Let me be certain I understand your use case correctly.

    There is a REST-based service that returns customer Identifiers object containing a customerId and accountNumber. Initially the caches are empty...

    # identifiers_cache_by_customer_id
    
    Customer ID | Identifier
    ------------------------
                |
    
    

    And...

    #identifiers_cache_by_account_number
    
    Account Number | Identifier
    ---------------------------
                   |
    
    

    Then, when a caller makes a call to 1 of the @Cacheable service methods of your @Service annotated CachedIdentifiersCache service, say by "account number" first, such as:

    String accountNumber = "abc123";
    
    Identifiers customerIdentifiers = 
      cacheableService.getIdentifier(accountNumber);
    

    And the Identifiers object returned for customer with account number "abc123" has a customer ID of 98765, you would to see in the caches, the following:

    # identifiers_cache_by_customer_id
    
    Customer ID | Identifier
    ------------------------
    98765       | Identifier@a102bf
    
    

    And:

    # identifiers_cache_by_account_number
    
    Account Number | Identifier
    ---------------------------
    abc123         | Identifier@a102bf
    
    

    That way, and subsequently, if another caller then calls either getIdentifiers(..) method with a customer ID (98765) or with account number ("abc123"), then it returns the cached Identifier@a102bf for that customer (either from the "identifiers_cache_by_customer_id" cache or "identifiers_cache_by_account_number" cache, respectively).

    Correct?

    I am now going to assume this is what you want.

    You can achieve the desired outcome in the following way, and technically, there are many more ways to achieve this outcome as well.

    You can simply inject the CacheManager bean into your service class like my following example will demonstrate and use it to update both caches appropriately.

    For example:

    @Service
    public class CachedIdentifiersCache {
    
        private final Cache identifiersCacheByCustomerId;
        private final Cache identifiersCacheByAccountNumber;
    
        private final LazyIdentifiersService slowServiceWithMultipleRestInvocations;
    
        public CachedIdentifiersCache(@NonNull CacheManager cacheManager,
            @NonNull LazyIdentifiersService remoteService) {
    
          Assert.notNull(cacheManager, "CacheManager is required");
          Assert.notNull(remoteService, "LazyIdentifiersService is required");
    
          this.slowServiceWithMultipleRestInvocations = remoteService;
    
          this.identifiersCacheByCustomerId = 
            cacheManager.getCache("identifiers_cache_by_customer_id");
    
          this.identifiersCacheByAccountNumber =
            cacheManager.getCache("identifiers_cache_by_account_number");
        }
    
        @Cacheable(
          value = {"identifiers_cache_by_customer_id"}, 
          key = "#customerId"
        )
        public Identifiers getCachedIdentifiers(Long customerId) {
    
          Identifiers identifiers = slowServiceWithMultipleRestInvocations
            .getIdentifiers(customerId);
    
          if (identifiers != null) {
            this.identifiersCacheByAccountNumber
              .put(identifiers.getAccountNumber(), identifiers);
          }
    
          return identifiers;
        }
    
    
        @Cacheable(
          value = {"identifiers_cache_by_account_number"}, 
          key = "#accountNumber"
        )
        public Identifiers getCachedIdentifiers(String accountNumber) {
    
          Identifiers identifiers = slowServiceWithMultipleRestInvocations
            .getIdentifiers(accountNumber);
    
          if (identifiers != null) {
            this.identifiersCacheByCustomerId
              .put(identifiers.getCustomerId(), identifiers);
          }
    
          return identifiers;
        }
    }
    
    

    Hopefully you recognize the changes here.

    Unfortunately, this approach requires a direct dependency on the caches in your application service class and forces your application code to cache the Identifiers object in the other (opposite) cache, by ID or accountNumber, manually.

    Other possible solutions...

    You could also achieve the same outcome by restructuring your code and using a combination of @Cacheable and @CachePut annotated methods. However, be careful with this approach since Spring AOP (caching) proxied methods cannot call other AOP proxied methods once inside the proxied object.

    In other words, an @Cacheable annotated method cannot call an @CachePut annotated method from inside the @Cacheable annotated method of the cacheable object (like CachedIdentifiersCache).

    You also cannot annotate the same service method with both @Cacheable and @CachePut like some may think, see here.

    You could also provide special wrappers around the CacheManager and/or Cache objects (the Spring Frameworks primary SPI for caching infrastructure and abstracting common caching providers) that handles cache pairs, like your use case loosely described above.

    You should also think about what would happen when 2 or more separate calling Threads (requests) calls the service class for the "same" logical customer, say our customer in the scenario above, where Thread 1 calls getIdentifiers(..) with customer ID 98765 and the Thread 2 simultaneously calls getIdentifiers(..) with account number "abc123".

    This could and most likely will result in the slowServiceWithMultipleRestInvocations being called at least 2 or more times (depending on the number of requests/Threads) for the same customer until the customer is properly cached by 1 of the calls (Threads).

    Spring does offer some coordination here, but it will not help in your case.

    Good luck!