How do you control HTTP caching for static resources in Spring MVC (cache-period, Cache-Control, ETags)?
answer
- setCachePeriod(seconds) vs setCacheControl(CacheControl)
- CacheControl.maxAge(...).cachePublic().immutable()
- auto Last-Modified + ETag -> 304
- immutable ONLY with fingerprinted URLs
- spring.web.resources.cache.cachecontrol.*
basics
~10 sUse setCachePeriod(seconds) or setCacheControl(CacheControl.maxAge(...)) on the resource handler registration to emit Cache-Control/Expires headers. Spring also auto-adds Last-Modified and ETag support for conditional requests.
solid answer
~30 sOn each addResourceHandler registration you configure caching. setCachePeriod(int seconds) sets a simple max-age; the richer setCacheControl(CacheControl) lets you build Cache-Control precisely — CacheControl.maxAge(Duration.ofDays(365)).cachePublic().immutable(). ResourceHttpRequestHandler automatically supports conditional GETs: it computes Last-Modified from the file and can emit ETags, returning 304 Not Modified when If-None-Match/If-Modified-Since match, saving bandwidth. In Spring Boot, spring.web.resources.cache.period and spring.web.resources.cache.cachecontrol.* set defaults for the auto-configured handlers. Best practice: pair long/immutable cache lifetimes with content-versioned (fingerprinted) URLs so you can cache aggressively yet still bust the cache on deploy. Without versioning, use short/moderate max-age plus ETag validation to stay correct.
code
java · 8 lines@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365))
.cachePublic()
.immutable());
}go deeper
Knows setCachePeriod exists to cache assets.
Can build a CacheControl and explain 304 conditional requests.
Explains pairing immutable long-lived cache with fingerprinting and ETag validation tradeoffs.
Reasons about CDN interplay, cache-public vs private, and deploy-time invalidation strategy end to end.
## Why caching matters Static assets rarely change between requests, so telling the browser to cache them avoids repeated downloads. HTTP caching is driven by response headers: `Cache-Control` (max-age, public/private, immutable), `Expires` (legacy), `Last-Modified`, and `ETag`. ## Setting cache lifetime On a resource handler registration: - `setCachePeriod(int seconds)` — the simple knob; emits `Cache-Control: max-age=<seconds>`. `0` means no caching, a negative/unset value emits no cache header. - `setCacheControl(CacheControl cc)` — the modern, expressive API. Build with the fluent `CacheControl` factory: - `CacheControl.maxAge(Duration.ofDays(365))` - `.cachePublic()` / `.cachePrivate()` - `.immutable()` — tells browsers never to revalidate during the lifetime (great for fingerprinted files) - `.noStore()` / `.noCache()` for sensitive or must-revalidate content. ## Conditional requests (validation) `ResourceHttpRequestHandler` supports **conditional GET** automatically: - It reads the resource's last-modified timestamp and sets the `Last-Modified` header. If the client sends `If-Modified-Since` and the file is unchanged, Spring returns **304 Not Modified** with an empty body. - **ETags**: an ETag is a content fingerprint. When you use `VersionResourceResolver` with a `ContentVersionStrategy`, the version hash serves as a strong validator; alternatively `ShallowEtagHeaderFilter` computes an ETag from the response body. On a matching `If-None-Match`, Spring returns 304. 304 responses save bandwidth (no body) even when max-age has expired, because the client just revalidates. ## Spring Boot defaults Auto-configured static handlers read: - `spring.web.resources.cache.period` (a Duration) — global max-age. - `spring.web.resources.cache.cachecontrol.*` — fine-grained (`max-age`, `no-store`, `must-revalidate`, `cache-public`, etc.). If you set any `cachecontrol.*`, it overrides `cache.period`. - `spring.web.resources.cache.use-last-modified` (default true). ## When to use what - **Fingerprinted assets** (app.<hash>.js): `maxAge(365d).immutable()` — cache forever; the URL changes on content change. - **Non-versioned assets**: short/moderate max-age + rely on ETag/Last-Modified validation for correctness. - **Sensitive/dynamic**: `noStore()`. ## Gotchas - `immutable()` without content versioning means stale assets after deploy — only pair it with fingerprinting. - `setCachePeriod` and `setCacheControl` on the same registration: `setCacheControl` wins (it's the richer one). - ETag computation via `ShallowEtagHeaderFilter` still reads the whole body into memory to hash it — it saves bandwidth, not server work.
- Why is immutable() dangerous without content versioning?immutable() tells browsers never to revalidate during the max-age. If the URL stays the same across deploys, clients keep serving the old cached file indefinitely. It's only safe when the filename/URL changes whenever content changes (fingerprinting).
- What does a 304 Not Modified save, and what does it not?It saves bandwidth — no response body is sent. It does not necessarily save server work: the server still resolves the resource and, for shallow ETags, may hash the full body to compare validators.
saying these in an interview costs you the question
- Setting immutable() on non-versioned files and expecting deploys to update clients
- Thinking setCachePeriod and ETags are mutually exclusive — both can apply
- Believing ETag validation eliminates all server work
- Confusing Cache-Control max-age with the legacy Expires as required