feat(api): add Redis caching for user quota and improve cache invalidation

Add 1-min TTL caching to CheckQuotaHandler (previously uncached, hitting
3 DB queries per guarded request). Add cache invalidation to
MeterUsageHandler and UpgradeSubscriptionHandler so quota caches stay
fresh after usage metering and plan changes. Increase search results TTL
from 1min to 2min per spec. Add market cache invalidation on listing
creation to keep district stats and market reports consistent.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 01:11:40 +07:00
parent 62f4f001b6
commit 05651ba4c3
10 changed files with 119 additions and 11 deletions

View File

@@ -149,9 +149,10 @@ describe('CacheService', () => {
describe('CacheTTL', () => {
it('should have correct TTL values', () => {
expect(CacheTTL.LISTING_DETAIL).toBe(300);
expect(CacheTTL.SEARCH_RESULTS).toBe(60);
expect(CacheTTL.SEARCH_RESULTS).toBe(120);
expect(CacheTTL.MARKET_DATA).toBe(1800);
expect(CacheTTL.USER_PROFILE).toBe(600);
expect(CacheTTL.USER_QUOTA).toBe(60);
});
});
});

View File

@@ -10,8 +10,8 @@ export const CACHE_MISS_TOTAL = 'cache_miss_total';
export const CacheTTL = {
/** Listing detail — moderate TTL, invalidated on mutation */
LISTING_DETAIL: 300, // 5 min
/** Search results — short TTL due to high variability */
SEARCH_RESULTS: 60, // 1 min
/** Search results — short TTL, invalidated on listing mutations */
SEARCH_RESULTS: 120, // 2 min
/** District stats — moderate TTL, invalidated on listing events */
DISTRICT_STATS: 300, // 5 min
/** Market report — moderate TTL, invalidated on listing events */
@@ -22,6 +22,8 @@ export const CacheTTL = {
MARKET_DATA: 1800, // 30 min
/** User profile — moderate TTL, invalidated on mutation */
USER_PROFILE: 600, // 10 min
/** User quota — very short TTL, invalidated on usage metering and plan changes */
USER_QUOTA: 60, // 1 min
} as const;
export enum CachePrefix {
@@ -33,6 +35,7 @@ export enum CachePrefix {
MARKET_HEATMAP = 'cache:market:heatmap',
MARKET_DISTRICT = 'cache:market:district',
USER_PROFILE = 'cache:user:profile',
USER_QUOTA = 'cache:user:quota',
}
@Injectable()