feat(cache): implement Redis caching layer for hot-read endpoints
Add cache-aside pattern for listing detail, search results, market analytics (4 endpoints), and user profile queries. Cache invalidation on all write mutations. Prometheus cache_hit_total/cache_miss_total metrics with resource labels. - CacheService: getOrSet, invalidate, invalidateByPrefix (SCAN-based) - TTLs: listing 5m, search 1m, market 30m, profile 10m - All 230 tests passing (13 new cache tests + 6 updated handler tests) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject, BadRequestException } from '@nestjs/common';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service';
|
||||
import { CreateListingCommand } from './create-listing.command';
|
||||
import { PROPERTY_REPOSITORY, type IPropertyRepository } from '../../../domain/repositories/property.repository';
|
||||
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
|
||||
@@ -22,6 +23,7 @@ export class CreateListingHandler implements ICommandHandler<CreateListingComman
|
||||
@Inject(PROPERTY_REPOSITORY) private readonly propertyRepo: IPropertyRepository,
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateListingCommand): Promise<CreateListingResult> {
|
||||
@@ -87,6 +89,8 @@ export class CreateListingHandler implements ICommandHandler<CreateListingComman
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
await this.cache.invalidateByPrefix(CachePrefix.SEARCH);
|
||||
|
||||
return {
|
||||
listingId,
|
||||
propertyId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { NotFoundException } from '@modules/shared/domain/domain-exception';
|
||||
import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service';
|
||||
import { ModerateListingCommand } from './moderate-listing.command';
|
||||
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
|
||||
|
||||
@@ -9,6 +10,7 @@ export class ModerateListingHandler implements ICommandHandler<ModerateListingCo
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(command: ModerateListingCommand): Promise<{ status: string }> {
|
||||
@@ -34,6 +36,11 @@ export class ModerateListingHandler implements ICommandHandler<ModerateListingCo
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
|
||||
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
|
||||
]);
|
||||
|
||||
return { status: listing.status };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { NotFoundException } from '@modules/shared/domain/domain-exception';
|
||||
import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service';
|
||||
import { UpdateListingStatusCommand } from './update-listing-status.command';
|
||||
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
|
||||
|
||||
@@ -9,6 +10,7 @@ export class UpdateListingStatusHandler implements ICommandHandler<UpdateListing
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateListingStatusCommand): Promise<{ status: string }> {
|
||||
@@ -32,6 +34,11 @@ export class UpdateListingStatusHandler implements ICommandHandler<UpdateListing
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
|
||||
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
|
||||
]);
|
||||
|
||||
return { status: listing.status };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { NotFoundException } from '@modules/shared/domain/domain-exception';
|
||||
import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service';
|
||||
import { GetListingQuery } from './get-listing.query';
|
||||
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
|
||||
|
||||
@@ -59,13 +60,23 @@ export interface ListingDetailDto {
|
||||
export class GetListingHandler implements IQueryHandler<GetListingQuery> {
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetListingQuery): Promise<ListingDetailDto> {
|
||||
const result = await this.listingRepo.findByIdWithProperty(query.listingId);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Listing', query.listingId);
|
||||
}
|
||||
return result as unknown as ListingDetailDto;
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.LISTING, query.listingId);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const result = await this.listingRepo.findByIdWithProperty(query.listingId);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Listing', query.listingId);
|
||||
}
|
||||
return result as unknown as ListingDetailDto;
|
||||
},
|
||||
CacheTTL.LISTING_DETAIL,
|
||||
'listing',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user