Spaces:
Sleeping
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
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
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
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.
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.