java
40 lines · 8 steps
How Spring caching layers a product lookup
A Spring service caches product reads, guards what gets stored, and evicts stale entries with declarative annotations.
Explained by
highlit
1@Service
2public class ProductLookupService {
3
4 private final ProductRepository productRepository;
5 private final ExternalCatalogClient catalogClient;
6
7 public ProductLookupService(ProductRepository productRepository, ExternalCatalogClient catalogClient) {
8 this.productRepository = productRepository;
9 this.catalogClient = catalogClient;
10 }
11
12 @Cacheable(cacheNames = "products", key = "#sku", unless = "#result == null")
13 public ProductDto findBySku(String sku) {
14 return productRepository.findBySku(sku)
15 .map(ProductDto::from)
16 .orElseGet(() -> fetchFromCatalog(sku));
17 }
18
19 @Cacheable(cacheNames = "productAvailability", key = "#sku", unless = "!#result.stocked")
20 public Availability checkAvailability(String sku) {
21 ProductDto product = findBySku(sku);
22 if (product == null) {
23 return Availability.unknown(sku);
24 }
25 return catalogClient.availability(sku);
26 }
27
28 @CacheEvict(cacheNames = {"products", "productAvailability"}, key = "#sku")
29 public void evict(String sku) {
30 }
31
32 private ProductDto fetchFromCatalog(String sku) {
33 return catalogClient.lookup(sku)
34 .map(remote -> {
35 Product saved = productRepository.save(remote.toEntity());
36 return ProductDto.from(saved);
37 })
38 .orElse(null);
39 }
40}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Declarative caching with @Cacheable keeps the caching logic out of your method bodies entirely.
- 2The unless attribute lets you skip caching for results that would poison the cache, like nulls or out-of-stock items.
- 3Pair every cache with an eviction path so stale entries can be invalidated on demand.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-spring-caching-layers-a-product-lookup-explained-java-33f8/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.