Quran_Tech_Server / docs /caching-architecture.md
aboalaa147's picture
Initial deployment
eb6a2f9
|
Raw
History Blame Contribute Delete
10.1 kB
# 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.