HTTP Caching — a browser/proxy mechanism to cache HTTP responses so data doesn't need to be fetched repeatedly.
Simple analogy: HTTP Cache is like a notepad on the fridge. You wrote "milk — available." Next time you don't check — just take it. If it says "expired" — go to the store (new request). Or think of it like a library: you borrow a book (GET request), the librarian stamps "return by next Monday" (max-age). Before that date, you can re-read it without asking the librarian again. After the date, you check if the book has been updated (ETag/Last-Modified).
Why it matters: Without caching, every browser refresh or page load triggers a full round-trip to your server. This wastes bandwidth, increases latency for users, and adds unnecessary load to your backend. Proper caching can reduce server load by 50-80% for read-heavy endpoints.
Headers:
Cache-Control — caching instructions.ETag — "hash" of the resource version.Last-Modified — last modification date.Vary — which request headers affect the cached version (e.g., Accept-Encoding).In Spring Boot:
1@RestController2@RequestMapping("/api/products")3public class ProductController {45 @GetMapping("/{id}")6 public ResponseEntity<Product> getProduct(@PathVariable Long id) {7 Product product = service.findById(id);8 return ResponseEntity.ok()9 .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))10 .eTag(String.valueOf(product.hashCode()))11 .body(product);12 }1314 @GetMapping15 public ResponseEntity<List<Product>> getAll() {16 return ResponseEntity.ok()17 .cacheControl(CacheControl.noCache()) // Do not cache18 .body(service.findAll());19 }2021 @GetMapping("/featured")22 public ResponseEntity<List<Product>> getFeatured() {23 return ResponseEntity.ok()24 .cacheControl(CacheControl.maxAge(24, TimeUnit.HOURS)25 .cachePrivate())26 .body(service.findFeatured());27 }28}
304 Not Modified:
1@GetMapping("/{id}")2public ResponseEntity<Product> getProduct(3 @PathVariable Long id,4 @RequestHeader(value = "If-None-Match", required = false) String etag) {5 Product product = service.findById(id);6 String currentEtag = String.valueOf(product.hashCode());78 if (currentEtag.equals(etag)) {9 return ResponseEntity.notModified().build(); // 30410 }11 return ResponseEntity.ok().eTag(currentEtag).body(product);12}
Cache-Control directives:
max-age=3600 — cache for 1 hour.no-cache — check before use (revalidate with server).no-store — do not cache at all (sensitive data).public — can be cached in CDN/proxy.private — browser only, not CDN.must-revalidate — once expired, must check with server before using.stale-while-revalidate=60 — serve stale content while revalidating in background.Server-side caching with Spring Cache:
1@Service2public class ProductService {3 @Cacheable(value = "products", key = "#id")4 public Product findById(Long id) {5 return productRepository.findById(id).orElseThrow();6 }78 @CacheEvict(value = "products", key = "#id")9 public void deleteProduct(Long id) {10 productRepository.deleteById(id);11 }1213 @CachePut(value = "products", key = "#result.id")14 public Product updateProduct(Product product) {15 return productRepository.save(product);16 }17}
Common pitfalls:
Vary: Accept-Encoding — different clients (gzip vs brotli) get wrong cached content.private for user-specific responses.