skip to content

How do you control HTTP caching for static resources in Spring MVC (cache-period, Cache-Control, ETags)?

level: middleimportance: should knowfreq 55%

answer

  1. setCachePeriod(seconds) vs setCacheControl(CacheControl)
  2. CacheControl.maxAge(...).cachePublic().immutable()
  3. auto Last-Modified + ETag -> 304
  4. immutable ONLY with fingerprinted URLs
  5. spring.web.resources.cache.cachecontrol.*

basics

~10 s

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

On 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
java
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/assets/**")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))
                    .cachePublic()
                    .immutable());
}

go deeper

for a junior

Knows setCachePeriod exists to cache assets.

for a middle

Can build a CacheControl and explain 304 conditional requests.

for a senior

Explains pairing immutable long-lived cache with fingerprinting and ETag validation tradeoffs.

for a principal

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

context