# Quran App — Caching Architecture --- ## 1. Overview The platform uses a two-layer caching strategy to reduce database load and eliminate unnecessary loading states for end users. | Layer | Technology | Scope | |---|---|---| | Server-side | Caffeine (in-process) via Spring Cache | Backend JVM | | Client-side | React Query (`TanStack Query`) | Browser memory | These two layers are independent but complementary. The server cache cuts repeated DB queries across all users. The client cache eliminates the loading spinner when a user navigates away and returns to a page they have already visited. --- ## 2. Server-Side Caching (Caffeine) ### 2.1 Why Caffeine Caffeine is a high-performance in-process cache for the JVM. It was chosen over Redis because the application runs as a single instance and does not require distributed cache synchronization. This keeps the infrastructure simple — no additional service to deploy or monitor. If the application is ever scaled horizontally across multiple instances, the caches would need to be externalized to Redis. ### 2.2 Configuration **File:** `quran-app-backend/src/main/java/com/yourcompany/quranapp/quran_app_backend/config/CacheConfig.java` All caches share a single default spec: - **TTL:** 10 minutes (expire after write) - **Max size:** 500 entries per cache - **Stats recording:** enabled — metrics are exposed at `/actuator/metrics/cache.gets` ```java Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(500) .recordStats() ``` Caches are pre-declared by name as constants on `CacheConfig` so service classes import the constant rather than hardcoding strings. ### 2.3 Cache Inventory #### Sheikh Caches | Cache name | Key | Cached method | Evicted by | |---|---|---|---| | `sheikhAvailability` | `sheikhId` | `SheikhAvailabilityService.getAvailability()` | `setAvailability()` | | `sheikhAvailableDates` | `sheikhId_from_days` | `getAvailableDatesForRange()` | `setAvailability()` | | `allActiveSheikhs` | `page_size` | `SheikhServiceFind.getAllActiveSheikhs()` | `updatePrice()`, sheikh approve/reject | | `sheikhProfile` | `sheikhId` | `SheikhServiceFind.getSheikhProfile()` | `updatePrice()` | | `sheikhTopReviews` | `sheikhId` | `SheikhServiceFind.getTopReviews()` | — (TTL only) | | `sheikhDashboardStats` | `sheikhId` | `sheikhDashboardService.getDashboardStats()` | `updatePrice()` | #### Student Caches | Cache name | Key | Cached method | Evicted by | |---|---|---|---| | `studentDashboard` | `userId` | `DashboardService.getStudentDashboard()` | `createRecitationFromAudio()` | | `studentRecitationStats` | `userId` | `StudentPracticeService.getRecitationStats()`, `StudentRecitationService.getRecitationSummaryStats()` | `createRecitationFromAudio()` | | `studentTopRecitations` | `userId` | `StudentRecitationService.getTop5RecentRecitations()` | `createRecitationFromAudio()` | | `studentStreak` | `userId` | `StudentStreakService.getUserStreakDto()` | `updateStreak()` | #### Admin Caches | Cache name | Key | Cached method | Evicted by | |---|---|---|---| | `adminDashboardStats` | — (single entry) | `mainDashbaordAdmin.getDashboardStatsAndPlatformAnalytics()` | `approveSheikh()`, `rejectSheikh()` | | `adminPendingSheikhs` | — (single entry) | `mainDashbaordAdmin.getDashboardPendingSheikhs()` | `approveSheikh()`, `rejectSheikh()`, `scheduleInterview()` | | `adminSheikhList` | — (single entry) | `sheikhService.getNotApprovedSheikhsByStatus()`, `UserManagementService.getSheikhs()` | `approveSheikh()`, `rejectSheikh()`, `scheduleInterview()` | | `adminStudentList` | — (single entry) | `UserManagementService.getStudents()` | `blockUser()`, `unblockUser()` | | `adminUserMgmtStats` | — (single entry) | `UserManagementService.getUserManagementStats()` | `blockUser()`, `unblockUser()` | ### 2.4 Cache Annotations Used | Annotation | Purpose | |---|---| | `@Cacheable` | Returns cached value if present; calls the method and stores the result if not | | `@CacheEvict` | Removes one or all entries from a named cache after a method executes | | `@Caching` | Groups multiple `@CacheEvict` (or mixed) annotations on a single method | ### 2.5 What Is Not Cached The following are deliberately left uncached: - **`getUpcomingClasses(sheikhId)`** — session state changes frequently (booking, cancellation, completion). Stale data here directly misleads the sheikh. - **`getAllSessionsForStudent()`** / **`getSessionStatus()`** — session lists are time-sensitive and mutation-heavy. - **`bookSession()`** — a write operation, never cached. - **Auth and JWT operations** — security-critical, must always hit the DB. --- ## 3. Client-Side Caching (React Query) ### 3.1 Why React Query React Query manages server state in the browser. Without it, every component mount triggers a fresh API call and shows a loading spinner. With it, previously fetched data is served instantly from an in-memory cache while a background refetch runs silently. ### 3.2 Query Key Convention Each major data domain has its own query key: | Query key | Component | API call | |---|---|---| | `['dashboard']` | `StudentDashboard` | `fetchDashboardData()` | | `['sheikhDashboard']` | `SheikhDashboard` | `fetchSheikhDashboardData()` | ### 3.3 Cache Settings Per Dashboard #### Student Dashboard ```ts useQuery({ queryKey: ['dashboard'], queryFn: fetchDashboardData, staleTime: 0, // always considered stale — refetches in background gcTime: 10 * 60 * 1000, // keep in memory for 10 min after last use refetchOnWindowFocus: true, refetchOnMount: true, }) ``` `staleTime: 0` means the student always gets a background refresh when they revisit the dashboard, but they never see a blank loading state because the cached data is shown immediately while the refresh runs. #### Sheikh Dashboard ```ts useQuery({ queryKey: ['sheikhDashboard'], queryFn: fetchSheikhDashboardData, staleTime: 5 * 60 * 1000, // treat as fresh for 5 minutes — no refetch on revisit gcTime: 10 * 60 * 1000, refetchOnWindowFocus: true, }) ``` `staleTime: 5 minutes` is appropriate here because the sheikh dashboard shows aggregated stats (earnings, session counts, ratings) that do not change second-by-second. A 5-minute window eliminates the loading spinner entirely on revisit while still keeping data reasonably fresh. ### 3.4 Optimistic Updates When the sheikh updates their hourly rate, the new price is written directly into the React Query cache without waiting for a refetch. This gives instant UI feedback. ```ts queryClient.setQueryData(['sheikhDashboard'], (old) => ({ ...old, data: { ...old.data, stats: { ...old.data.stats, hourlyRate: newPrice }, }, })); ``` --- ## 4. Cache Invalidation Flow The diagram below shows how a write operation propagates through both cache layers. ``` Sheikh updates price │ ▼ PUT /api/sheikh/.../update-price │ ├─► @CacheEvict: sheikhDashboardStats (sheikhId) ├─► @CacheEvict: sheikhProfile (sheikhId) └─► @CacheEvict: allActiveSheikhs (all entries) │ ▼ Next GET request hits DB, repopulates cache with fresh data │ Client side: queryClient.setQueryData(['sheikhDashboard'], ...) │ ▼ UI updates instantly (optimistic) ``` For admin approval/rejection of a sheikh: ``` Admin approves sheikh │ ▼ POST /api/admin/sheikhs/{id}/approve │ ├─► @CacheEvict: adminPendingSheikhs (all) ├─► @CacheEvict: adminSheikhList (all) ├─► @CacheEvict: adminDashboardStats (all) └─► @CacheEvict: allActiveSheikhs (all) ``` For a student submitting a new recitation: ``` Student submits recitation │ ▼ POST /api/student/recitations │ ├─► @CacheEvict: studentRecitationStats (userId) ├─► @CacheEvict: studentTopRecitations (userId) └─► @CacheEvict: studentDashboard (userId) ``` --- ## 5. Monitoring Cache hit/miss rates are available via Spring Boot Actuator without any additional configuration: ``` GET /actuator/metrics/cache.gets GET /actuator/metrics/cache.gets?tag=name:sheikhDashboardStats&tag=result:hit GET /actuator/metrics/cache.gets?tag=name:sheikhDashboardStats&tag=result:miss ``` The `recordStats()` setting on the Caffeine spec enables this. A high miss rate on a specific cache indicates the TTL is too short or the eviction strategy is too aggressive for its access pattern. --- ## 6. Design Decisions and Tradeoffs ### Single-entry admin caches use `allEntries = true` Admin caches for stats and lists do not use a per-entity key because the data aggregates the whole platform. `allEntries = true` is used on eviction because there is only one meaningful entry anyway. ### Student recitation stats are keyed by `userId` Each student's stats are independent, so per-user keying is correct. Evicting one student's cache on recitation submission does not affect any other student. ### Sheikh availability dates cache uses a compound key `sheikhId_from_days` captures the full context of the query. A sheikh with availability Monday–Friday will have different available dates depending on whether `from` is this week or next month, so `sheikhId` alone would be incorrect. ### `sheikhTopReviews` relies on TTL only Reviews are written infrequently and the top-3 display is decorative. Adding eviction on review creation would require wiring the review write path into the cache eviction chain. The 10-minute TTL is an acceptable tradeoff for this use case. ### Client `staleTime` differs between student and sheikh dashboards The student dashboard has `staleTime: 0` because it shows live practice data (streaks, recent recitations) that the student actively creates. The sheikh dashboard has `staleTime: 5 minutes` because its data is aggregated and less volatile.