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
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.