skip to content

How do you add a Cache-Control response header to a Spring MVC controller method, and what does the CacheControl builder give you?

level: juniorimportance: must knowfreq 62%

answer

  1. org.springframework.http.CacheControl builder
  2. ResponseEntity.ok().cacheControl(...)
  3. maxAge / noStore / noCache / cachePublic-Private
  4. header policy, NOT server-side caching
  5. also static resources + WebContentInterceptor

basics

~10 s

Return 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 s

Spring'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 lines
java
import 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

for a junior

Know it's a fluent builder set via ResponseEntity.cacheControl and that maxAge/public are the common knobs.

for a middle

Distinguish no-cache vs no-store, public vs private, and know it writes headers only.

for a senior

Combine max-age freshness with ETag validation; apply CacheControl to static resources and interceptors; use private/sMaxAge deliberately for CDNs.

for a principal

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

context