How do you add a Cache-Control response header to a Spring MVC controller method, and what does the CacheControl builder give you?
answer
- org.springframework.http.CacheControl builder
- ResponseEntity.ok().cacheControl(...)
- maxAge / noStore / noCache / cachePublic-Private
- header policy, NOT server-side caching
- also static resources + WebContentInterceptor
basics
~10 sReturn a ResponseEntity and call .cacheControl(...) with Spring's CacheControl builder, e.g. CacheControl.maxAge(Duration.ofMinutes(10)).cachePublic(). Spring writes the correct Cache-Control header (max-age=600, public) so browsers/proxies can cache the response.
solid answer
~30 sSpring's org.springframework.http.CacheControl is a fluent builder that produces a valid Cache-Control header string, so you don't hand-write directives. The common path is ResponseEntity.ok().cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)).cachePublic()).body(dto). maxAge sets max-age (freshness lifetime); cachePublic/cachePrivate control shared vs per-user caches; noStore forbids storing at all; noCache forces revalidation before reuse; mustRevalidate, sMaxAge (shared caches), staleWhileRevalidate and staleIfError map to the matching directives. Beyond controllers you can attach it to static resources (ResourceHandlerRegistration.setCacheControl) or globally via a WebContentInterceptor. The builder is just header construction — it does no server-side caching itself; it only tells clients and intermediaries how they may cache the response.
code
java · 29 linesimport org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductService service;
ProductController(ProductService service) { this.service = service; }
@GetMapping("/{id}")
ResponseEntity<ProductDto> get(@PathVariable long id) {
ProductDto dto = service.find(id);
CacheControl cc = CacheControl.maxAge(Duration.ofMinutes(10))
.cachePublic();
// Cache-Control: max-age=600, public
return ResponseEntity.ok().cacheControl(cc).body(dto);
}
@GetMapping("/me")
ResponseEntity<ProfileDto> myProfile() {
// user-specific: never store in shared caches, always revalidate
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache().cachePrivate())
.body(service.currentUser());
}
}go deeper
Know it's a fluent builder set via ResponseEntity.cacheControl and that maxAge/public are the common knobs.
Distinguish no-cache vs no-store, public vs private, and know it writes headers only.
Combine max-age freshness with ETag validation; apply CacheControl to static resources and interceptors; use private/sMaxAge deliberately for CDNs.
Reason about CDN/shared-cache correctness (private vs s-maxage), stale-while-revalidate for resilience, and separating transport caching from application caching in the architecture.
## What Cache-Control is `Cache-Control` is an HTTP response header that tells browsers and intermediary caches (CDNs, proxies) whether and how long they may reuse a response instead of re-requesting it. Example: `Cache-Control: max-age=600, public` means "any cache may store this and serve it without asking the server again for 600 seconds." ## Spring's CacheControl builder Hand-writing the header string is error-prone, so Spring provides `org.springframework.http.CacheControl` — a **fluent builder** whose terminal object serializes to a correct header value. You never call a `build()` — you pass the `CacheControl` instance to Spring, which serializes it. Key factory + builder methods: - `CacheControl.maxAge(Duration)` / `maxAge(long, TimeUnit)` → `max-age=<seconds>`. The freshness lifetime. - `CacheControl.noStore()` → `no-store`. Nothing may be cached anywhere (use for sensitive data). - `CacheControl.noCache()` → `no-cache`. May be stored but must revalidate with the origin before every reuse. - `CacheControl.empty()` → no directives; a base to add things like `mustRevalidate()`. - On the returned builder: `.cachePublic()` → `public`; `.cachePrivate()` → `private` (only the end-user's browser, not shared caches); `.mustRevalidate()`; `.proxyRevalidate()`; `.sMaxAge(Duration)` → `s-maxage` (overrides max-age for shared caches); `.staleWhileRevalidate(Duration)`; `.staleIfError(Duration)`; `.noTransform()`. ## Where to apply it 1. **Per handler** — `ResponseEntity.ok().cacheControl(cc).body(x)`. Most common and most granular. 2. **Static resources** — `registry.addResourceHandler("/static/**").addResourceLocations(...).setCacheControl(cc)` inside a `WebMvcConfigurer`. 3. **Broad rules** — a `WebContentInterceptor` with `setCacheControl(cc)` applied across path patterns. ## Critical gotcha `CacheControl` performs **no server-side caching**. It only writes a header describing caching *policy* to clients and proxies. Don't confuse it with Spring's `@Cacheable` / `CacheManager` abstraction, which caches method results in-process (Caffeine/Redis/etc.). They are unrelated: `CacheControl` = HTTP transport caching; `@Cacheable` = application-level result caching. ## Freshness vs validation `max-age` gives **freshness** (reuse without asking). ETag/Last-Modified give **validation** (ask the server "still valid?" and get a cheap `304 Not Modified` if so). Real APIs often combine both: a short `max-age` plus an `ETag`, so caches serve fresh copies for a while and then revalidate cheaply. ## Edge cases - If you set no `Cache-Control`, browser heuristics may cache GETs unpredictably; being explicit is safer. - `private` matters when a response embeds user-specific data behind a shared CDN — otherwise one user's data could be served to another. - `no-store` is stronger than `no-cache`: `no-cache` still allows storage (just mandatory revalidation), which surprises people.
- What is the difference between no-cache and no-store?no-store forbids caches from keeping any copy at all (for sensitive data). no-cache allows storing the response but requires the cache to revalidate with the origin before every reuse — it can still serve a 304-validated copy.
- Does CacheControl.maxAge cache anything on the server?No. It only writes the Cache-Control header telling clients/proxies how long they may reuse the response. Server-side result caching is a separate concern (@Cacheable / CacheManager).
saying these in an interview costs you the question
- Thinking CacheControl caches responses on the server
- Believing no-cache means 'do not store' (that's no-store)
- Using cachePublic() for user-specific responses behind a CDN
- Hand-concatenating the Cache-Control header string instead of the builder