Table of contents
Cache Handling (Service Layer → Cache Interceptor)
In the existing code, the service layer checked for cached data and returned it if available, or performed the business logic and cached the return value if not. However, this approach was inefficient in terms of maintenance and code readability since the cache handling was mixed with the business logic in the service layer. To address this, a cache interceptor, called CacheInterceptor
, was created to handle caching separately. It was applied in the controller using @UseInterceptors()
.
CacheInterceptor
code
@Injectable()
export class CacheInterceptor implements NestInterceptor {
protected cacheMethods = ['GET'];
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
// Apply caching only for GET requests to retrieve recommended hospitals
if (this.isRequestGet(context)) {
const request = context.switchToHttp().getRequest();
const cacheKey = this.generateCacheKey(request);
console.log('cacheKey:', cacheKey);
// Check if cached data exists
const cachedData = await this.cacheManager.get(cacheKey);
if (cachedData) {
console.log('Using cached data.');
return of(cachedData);
}
// If cached data does not exist, handle the request
return next.handle().pipe(
tap((data) => {
// Store the data in the cache
console.log('Storing data in the cache.');
this.cacheManager.set(cacheKey, data);
}),
);
}
}
private generateCacheKey(request): string {
// Generate a unique cache key based on the request URL and query parameters
const reportId = request.params.report_id;
const radius = request.query.radius;
const maxCount = request.query.max_count;
return `${reportId}:${radius}:${maxCount}`;
}
private isRequestGet(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.cacheMethods.includes(req.method);
}
}
Cache Invalidation
When fetching the list of recommended hospitals on the frontend, if a user refreshes the page within 1 minute of the initial request, the cached data is retrieved. However, when a patient applies for or withdraws a hospital transfer, the screen should immediately reflect the appearance or disappearance of a notification window and transfer request cancellation button for the hospital to which the patient applied. However, due to the use of cached data, this immediate update was not possible. To address this issue, the ClearCacheInterceptor
was implemented to delete the cached data associated with the specific report_id
when a transfer request or cancellation occurs. Since there can be multiple cached data with the same report_id
but different radius
and max_count
query parameters, all cache keys starting with the report_id
were cleared.
ClearCacheInterceptor
code
@Injectable()
export class ClearCacheInterceptor implements NestInterceptor {
protected clearCacheMethods = ['POST', 'DELETE'];
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
if (this.isRequestPostOrDelete(context)) {
const request = context.switchToHttp().getRequest();
// 1. Pass the report_id to the method
await this.clearCacheKeysStartingWith(request.params.report_id);
}
return next.handle();
}
private async clearCacheKeysStartingWith(reportId: string): Promise<void> {
const
cacheKeys = await this.getCacheKeysStartingWith(reportId);
// 4. Iterate through the filtered keys and delete them from Redis
await Promise.all(cacheKeys.map((key) => this.cacheManager.del(key)));
}
private async getCacheKeysStartingWith(prefix: string): Promise<string[]> {
// 2. Retrieve all keys stored in Redis
const cacheKeys = await this.cacheManager.store.keys('*');
// 3. Filter keys that start with report_id:
return cacheKeys.filter((key) => key.startsWith(`${prefix}:`));
}
private isRequestPostOrDelete(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.clearCacheMethods.includes(req.method);
}
}