From 50f554680c8ccc6155f36880515f5b8a46b8787c Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 19:56:50 +0200 Subject: [PATCH] refactor(document): drop the 5-minute Cache-Control TTL on /density (#709) The density chart is an interactive filter control; a 5-minute private browser cache let it show stale month counts after an edit/upload/re-tag. The in-memory aggregation is sub-200ms p95 over ~5k docs, so there is no load reason to cache. Removing the explicit header lets Spring Security's default no-store directive apply, so the response is always fresh. Co-Authored-By: Claude Opus 4.8 --- .../familienarchiv/document/DocumentController.java | 6 +----- .../raddatz/familienarchiv/document/DocumentService.java | 6 ++++-- .../familienarchiv/document/DocumentControllerTest.java | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java index b2ee5f34..32fa0bc2 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentController.java @@ -3,7 +3,6 @@ package org.raddatz.familienarchiv.document; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; -import java.util.concurrent.TimeUnit; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -49,7 +48,6 @@ import org.raddatz.familienarchiv.filestorage.FileService; import org.raddatz.familienarchiv.user.UserService; import org.springframework.data.domain.Sort; import org.springframework.security.core.Authentication; -import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -406,9 +404,7 @@ public class DocumentController { TagOperator operator = "OR".equalsIgnoreCase(tagOp) ? TagOperator.OR : TagOperator.AND; DocumentDensityResult result = documentService.getDensity( new DensityFilters(q, senderId, receiverId, tags, tagQ, status, operator)); - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePrivate()) - .body(result); + return ResponseEntity.ok(result); } // --- TRAINING LABELS --- diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index 2f3edbc2..0aa824e5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -137,8 +137,10 @@ public class DocumentService { *

Implementation note: groups in memory rather than via SQL GROUP BY * because the existing {@link Specification} predicates compose easily * with {@code findAll(spec)} and the archive size (≈5k docs) keeps this - * well under the 200ms p95 target. Cache-Control: max-age=300 on the - * controller layer absorbs repeated browse loads. + * well under the 200ms p95 target. The controller sets no explicit + * Cache-Control, so the response is served fresh on every load (issue + * #709) — the recompute is imperceptible and stale month counts after an + * edit would be misleading on an interactive chart. * *

Tracked in issue #481 for re-evaluation when {@code documents > 50k} * — at that scale move the aggregation into SQL (GROUP BY TO_CHAR(meta_date, diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java index f1e34554..b655e0bf 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentControllerTest.java @@ -1423,16 +1423,16 @@ class DocumentControllerTest { @Test @WithMockUser - void density_emitsPrivateCacheControlHeader() throws Exception { + void density_isNeverBrowserCached() throws Exception { when(documentService.getDensity(any())).thenReturn( new DocumentDensityResult(List.of(), null, null)); + // The endpoint sets no explicit Cache-Control, so Spring Security's + // default no-store directive applies — the density chart is always fresh. mockMvc.perform(get("/api/documents/density")) .andExpect(status().isOk()) .andExpect(header().string("Cache-Control", - org.hamcrest.Matchers.containsString("max-age=300"))) - .andExpect(header().string("Cache-Control", - org.hamcrest.Matchers.containsString("private"))); + "no-cache, no-store, max-age=0, must-revalidate")); } @Test