Design a static-asset cache-busting strategy in Spring MVC using content versioning. Contrast ContentVersionStrategy with FixedVersionStrategy and address URL generation.
answer
- fingerprint URL: same content=same URL, changed=new URL
- ContentVersionStrategy = per-file MD5; FixedVersionStrategy = build-wide prefix
- must generate URLs: ResourceUrlProvider + ResourceUrlEncodingFilter
- CssLinkResourceTransformer for intra-CSS links
- if JS bundler already hashes, skip Spring versioning
basics
~10 sEnable a VersionResourceResolver with addContentVersionStrategy (per-file MD5 hash in the filename) so each file's URL changes only when its content changes. Serve with long, immutable Cache-Control, and generate versioned URLs via ResourceUrlProvider in templates.
solid answer
~40 sThe goal: cache assets aggressively (max-age far future, immutable) yet never serve stale bytes after deploy. VersionResourceResolver embeds a version token in the URL so a content change yields a new URL, auto-invalidating caches. Two strategies: ContentVersionStrategy computes an MD5 per file (app-9f8a2c.js) — fine-grained, only changed files bust; ideal default. FixedVersionStrategy inserts a build-wide token (e.g. /v3/app.js) — coarse, everything busts each release; useful for modules or ESM where filename hashing is awkward. Both require URL generation to match: templates must emit the versioned path via ResourceUrlProvider (Thymeleaf @{...} + ResourceUrlEncodingFilter), and CssLinkResourceTransformer rewrites intra-CSS links. Pair with resourceChain(true) for caching and immutable Cache-Control. In practice, a JS build tool often already fingerprints; then Spring just serves them and you skip VersionResourceResolver, relying on the build's manifest.
code
java · 19 lines@Configuration
public class AssetConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365))
.cachePublic().immutable())
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"));
}
// Enables th:href="@{/static/app.js}" -> /static/app-<md5>.js in views
@Bean
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}
}go deeper
Not expected to design this.
Can enable content versioning but may miss the URL-generation half.
Designs versioning + immutable caching and wires URL generation and CSS transformers.
Weighs server- vs build-tool-owned fingerprinting, CDN cache keys, entry-doc revalidation, and content vs fixed strategy tradeoffs holistically.
## The problem being solved You want two things that conflict: (1) maximum caching so browsers/CDNs never re-fetch unchanged assets, and (2) instant propagation of new asset versions after a deploy. **Content versioning (fingerprinting)** resolves the conflict: bake a version token derived from content into the URL. Same content -> same URL (cached forever); changed content -> new URL (fresh fetch). Then you can safely set `Cache-Control: max-age=31536000, immutable`. ## Spring's mechanism: VersionResourceResolver + VersionStrategy `VersionResourceResolver` sits in the resource chain. For incoming requests it extracts the version from the path, validates it against the actual resource, and delegates the cleaned path onward (404 on mismatch). You attach one or more `VersionStrategy` implementations keyed by path pattern: ### ContentVersionStrategy - `addContentVersionStrategy("/**")` - Computes an **MD5 hash of the file content** and inserts it into the filename: `app.js` -> `app-9f8a2c1e2b....js`. - **Fine-grained**: only files whose bytes changed get new URLs; unchanged files keep long-cached URLs across deploys. Best default for correctness and cache efficiency. - Cost: hashing content (mitigated by CachingResourceResolver). ### FixedVersionStrategy - `addFixedVersionStrategy("<version>", "/**")` - Inserts a **single build-wide token as a path prefix**: `/v3/app.js`, `/{buildNumber}/...`. - **Coarse**: every asset's URL changes on each version bump even if unchanged — busts the entire cache per release. - Useful when filenames can't carry a hash (e.g. ES modules that import by literal name, or third-party bundles) or when you want a simple global version. You can combine: fixed strategy for a directory of modules, content strategy for the rest, since strategies are pattern-keyed and evaluated in registration order. ## Making URLs match (the often-missed half) Resolving `/app-<hash>.js` is useless unless your HTML actually links to that URL. Spring generates versioned URLs by running the chain in the **resolveUrlPath** direction: - **ResourceUrlProvider** — the programmatic API (`getForLookupPath("/app.js")` -> `/app-9f8a2c.js`). - **ResourceUrlEncodingFilter** — wraps the response so `HttpServletResponse.encodeURL` and view technologies transparently rewrite resource URLs. - **Thymeleaf** `th:href="@{/app.js}"` / **JSP** integrate with this automatically when the filter is registered. - **CssLinkResourceTransformer** — rewrites `url(...)`/`@import` inside CSS so referenced images/fonts also get versioned URLs. Enable via the transformer chain. ## Cache headers to pair with it - `setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic().immutable())` on the handler. - The HTML entry document itself must stay revalidatable (short max-age / no-cache) so clients pick up the new fingerprinted URLs. ## Architectural decision: who owns fingerprinting? Modern SPA build tools (Vite, webpack, esbuild) already fingerprint and emit a manifest. Then: - Spring just serves the already-hashed files with immutable Cache-Control; you do **not** add VersionResourceResolver (double-hashing is pointless), and the frontend references assets via its own manifest. - Use Spring's VersionResourceResolver when the server owns asset delivery (server-rendered templates, WebJars, no JS bundler doing hashing). ## Gotchas - Forgetting URL generation: assets resolve when requested directly but templates still emit unversioned URLs -> no cache busting. Register `ResourceUrlEncodingFilter`. - `immutable()` without versioning = stale forever after deploy. - MD5 hashing is for cache-keying, not security — fine here. - CDN in front: the versioned path is part of the cache key (URLs differ per version) and should forward Cache-Control. - FixedVersionStrategy with a timestamp busts caches even on no-op redeploys — use a content-derived or stable build id.
- Your assets resolve when requested by their hashed URL, but the browser still requests the unhashed name and gets 404-or-uncached. What's wrong?URL generation isn't wired up. VersionResourceResolver only resolves incoming versioned paths; templates must emit versioned URLs via ResourceUrlProvider / ResourceUrlEncodingFilter (and Thymeleaf @{...}). Without the filter, views output the logical path and no cache busting occurs.
- When would you NOT use Spring's VersionResourceResolver at all?When a frontend build tool (Vite/webpack) already fingerprints assets and produces a manifest. Then Spring should just serve the pre-hashed files with immutable Cache-Control; adding VersionResourceResolver would redundantly re-hash and complicate URL generation.
- How does ContentVersionStrategy differ from FixedVersionStrategy in cache efficiency?ContentVersionStrategy hashes each file, so only changed files get new URLs — unchanged assets stay cached across deploys. FixedVersionStrategy uses one build-wide token, so every asset URL changes each release, invalidating the whole cache even for unchanged files.
saying these in an interview costs you the question
- Setting up VersionResourceResolver but not ResourceUrlEncodingFilter/ResourceUrlProvider, so URLs are never versioned
- Double-fingerprinting when the JS bundler already hashes
- Applying immutable Cache-Control to the HTML entry document too, freezing the whole app
- Believing FixedVersionStrategy only busts changed files — it busts everything per build
- Thinking MD5 here is a security concern