Cache storefront price lookups

Java · Spring Boot · intermediate · modification

The product grid calls `priceFor` once per tile, and the contract-price lookup behind it is now a high-volume query. This turns on Spring's cache abstraction with a new `CacheConfig` (`@EnableCaching` and an in-memory `ConcurrentMapCacheManager` holding a `prices` cache) and puts `@Cacheable` on `PriceService.priceFor`, keyed on the SKU so the grid, the cart and checkout all hit the same entries. There is no eviction: prices only change in the nightly ERP import, and the instances restart after it.

B2B storefront on Spring Boot 3.3.5 (Java 21), built with the Spring Boot parent POM (the compiler keeps parameter names, so SpEL can refer to `#buyer` and `#sku`), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16, Spring Security with sessions. Every `/api/storefront/**` request needs a signed-in buyer; `@AuthenticationPrincipal` resolves a `ShopUser(long userId, long companyId, String email)` record that is never null. Every buyer belongs to exactly one company, and a company usually has several buyers. Existing and unchanged: `StorefrontPriceController` (`GET /api/storefront/products/{sku}/price`), `CartService` and `CheckoutService`, which call `priceFor` with the signed-in buyer for each line; the `Product` entity (`sku` id, `listPrice` numeric(12,2), `active`), `ProductRepository.findBySkuAndActiveTrue`, the `ContractPrice` entity (unique on `companyId` + `sku`), `ContractPriceRepository.findByCompanyIdAndSku`, the immutable `PriceView(sku, unitPrice, contractPrice)` record, and `ProductNotFoundException` (`@ResponseStatus(NOT_FOUND)`). Products, list prices and contract prices change only in the nightly ERP import, which runs in the maintenance window with the storefront stopped; the instances start fresh afterwards, so nothing changes under a running instance. The application had no caching before this PR and has no other `CacheManager`. Sizes: about 150 buyer companies and 2,000 active SKUs, so at most 300,000 company and SKU pairs, which the heap is sized for.

Requirements

Files touched

--- src/main/java/com/acme/b2b/config/CacheConfig.java
+package com.acme.b2b.config;
+
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+    @Bean
+    public CacheManager cacheManager() {
+        return new ConcurrentMapCacheManager("prices");
+    }
+}
+
--- src/main/java/com/acme/b2b/pricing/PriceService.java
 import com.acme.b2b.catalog.ProductRepository;
 import com.acme.b2b.security.ShopUser;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
     }
 
+    @Cacheable(cacheNames = "prices", key = "#sku")
     @Transactional(readOnly = true)
     public PriceView priceFor(ShopUser buyer, String sku) {

Review this PR

Java practice