# 🎯 Analytics Module Architecture Exploration β€” Complete ## Documents Created I've prepared **3 comprehensive guides** for you: ### 1️⃣ **analytics_architecture_guide.md** (36 KB) **The Comprehensive Reference** β€” Read this first for deep understanding - βœ… DDD layer breakdown with code examples - βœ… All 24 endpoints documented - βœ… Query handler patterns (2 styles: @Cacheable vs manual) - βœ… Complete caching system (Redis patterns, TTLs, invalidation) - βœ… Real code examples from `GetMarketSnapshotHandler`, `GetDistrictStatsHandler` - βœ… Full Prisma schema for Property, Listing, MarketIndex, Valuation - βœ… Shared module utilities (CacheService, Cacheable decorator, interceptor) - βœ… **7-step guide: How to add GET /analytics/trending-areas endpoint** - βœ… Testing patterns - βœ… Error handling conventions ### 2️⃣ **quick_reference.md** (8 KB) **The Visual Quick Start** β€” Use this to navigate fast - βœ… Layer stack diagram (Presentation β†’ Application β†’ Domain β†’ Infrastructure) - βœ… Request flow example (HTTP β†’ Controller β†’ QueryHandler β†’ Cache β†’ DB) - βœ… Caching strategy matrix (when to cache, TTLs, prefixes) - βœ… Decorators & guards cheat sheet - βœ… Prisma schema snapshot - βœ… Response structure with/without cache metadata - βœ… 7-step endpoint addition checklist ### 3️⃣ **file_paths_reference.md** (8 KB) **The Navigation Map** β€” Find files & understand structure - βœ… Core module files (analytics.module.ts, index.ts) - βœ… All 24 endpoints mapped to file paths - βœ… DTO files organized by type (request vs response) - βœ… All 15+ query types with descriptions - βœ… Domain, Infrastructure, and Shared layer breakdowns - βœ… Database schema models with fields & indexes - βœ… Directory tree with line counts - βœ… Import patterns reference - βœ… Key metrics & numbers --- ## ✨ Key Findings ### Architecture Pattern ``` Domain-Driven Design (DDD) + CQRS (Command Query Responsibility Segregation) 4-layer structure: Presentation β†’ Application β†’ Domain β†’ Infrastructure ``` ### Controllers (Entry Points) - **AnalyticsController**: 19 endpoints (`/analytics/...`) - **AvmController**: 5 endpoints (`/avm/...`) - **Total**: 24 endpoints, all with guards (JWT, Quota, Rate Limit) ### Query Handlers: 2 Caching Patterns **Pattern 1: @Cacheable Decorator** (Simpler) ```ts @QueryHandler(GetDistrictStatsQuery) export class GetDistrictStatsHandler { @Cacheable({ prefix: CachePrefix.MARKET_DISTRICT, ttl: CacheTTL.DISTRICT_STATS, resource: 'district_stats', keyFrom: (query) => [query.city, query.period], }) async execute(query): Promise { return this.marketIndexRepo.getDistrictStats(query.city, query.period); } } ``` **Pattern 2: cache.getOrSet()** (For complex computation) ```ts async execute(query): Promise { const cacheKey = CacheService.buildKey(CachePrefix.MARKET_SNAPSHOT, query.city); return await this.cache.getOrSet( cacheKey, () => this.computeSnapshot(query.city), // Heavy computation CacheTTL.MARKET_SNAPSHOT, // TTL in seconds 'market_snapshot', // Prometheus label ); } ``` ### Redis Caching Strategy - **Cache-aside pattern**: Try Redis β†’ if miss, call loader, store result - **Envelope format**: `{ __v: data, cachedAt: ISO, ttlSeconds: 300 }` - **Graceful degradation**: If Redis down, calls loader directly (no error) - **Metrics**: `cache_hit_total`, `cache_miss_total`, `cache_degradation_total` - **TTLs**: Dashboard=300s, Reports=900s, Trends=1800s, Predictions=NO_CACHE ### Prisma Schema - **Property**: id, type, address, district, city, location (PostGIS Point), area, rooms, etc. - **Listing**: id, propertyId, sellerId, status, priceVND (BigInt), aiPriceEstimate, publishedAt - **MarketIndex**: district, city, propertyType, period; medianPrice (BigInt), avgPriceM2, stats - **Valuation**: id, propertyId, estimatedPrice, confidence, method, features (Json) ### DDD Layers 1. **Presentation**: Controllers, DTOs, Interceptors 2. **Application**: Query/Command Handlers (@QueryHandler, @CommandHandler) 3. **Domain**: Entities, Repository Interfaces, Service Interfaces 4. **Infrastructure**: Prisma Repositories, HTTP Services, External clients ### Shared Module Utilities - **CacheService**: Core cache-aside with Redis - **@Cacheable**: Method decorator for handlers - **CacheMetaInterceptor**: Wraps responses with `{ data, cacheMeta }` - **LoggerService**: Winston-based logging - **PrismaService**: ORM wrapper - **RedisService**: Redis client wrapper - **Guards**: JWT, Quota, Rate Limit --- ## πŸš€ Quick Start: Adding New Endpoint Example: **GET /analytics/trending-areas** ### 7-Step Process 1. **Request DTO** (`presentation/dto/get-trending-areas.dto.ts`) ```ts export class GetTrendingAreasDto { @IsOptional() city?: string = 'Hα»“ ChΓ­ Minh'; @IsOptional() propertyType?: PropertyType; @IsOptional() @Min(1) limit?: number = 10; } ``` 2. **Query Class** (`application/queries/get-trending-areas/get-trending-areas.query.ts`) ```ts export class GetTrendingAreasQuery { constructor( public readonly city: string, public readonly propertyType: PropertyType | undefined, public readonly limit: number, ) {} } ``` 3. **Handler** (`application/queries/get-trending-areas/get-trending-areas.handler.ts`) ```ts @QueryHandler(GetTrendingAreasQuery) export class GetTrendingAreasHandler implements IQueryHandler { @Cacheable({ prefix: CachePrefix.TRENDING_AREAS, ttl: CacheTTL.TRENDING_AREAS, resource: 'trending_areas', keyFrom: (query) => [query.city, query.propertyType, query.limit], }) async execute(query: GetTrendingAreasQuery): Promise { // Your logic here return { city: query.city, areas: [...], cachedAt: null, nextRefreshAt: null }; } } export interface GetTrendingAreasDto { city: string; areas: TrendingAreaDto[]; cachedAt: string | null; nextRefreshAt: string | null; } ``` 4. **Register Handler** (in `analytics.module.ts`) ```ts const QueryHandlers = [ // ... existing GetTrendingAreasHandler, // Add here ]; ``` 5. **Controller Method** (in `analytics.controller.ts`) ```ts @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, QuotaGuard) @RequireQuota('analytics_queries') @Get('trending-areas') @ApiOperation({ summary: 'Get trending districts' }) @ApiResponse({ status: 200, description: 'Trending areas retrieved' }) async getTrendingAreas(@Query() dto: GetTrendingAreasDto): Promise { return this.queryBus.execute( new GetTrendingAreasQuery(dto.city || 'Hα»“ ChΓ­ Minh', dto.propertyType, dto.limit || 10), ); } ``` 6. **Export DTOs** (in `presentation/dto/index.ts`) ```ts export * from './get-trending-areas.dto'; ``` 7. **Test** (in `__tests__/get-trending-areas.handler.spec.ts`) ```ts it('should return trending areas', async () => { const query = new GetTrendingAreasQuery('Hα»“ ChΓ­ Minh', undefined, 10); const result = await handler.execute(query); expect(result.areas).toBeDefined(); }); ``` --- ## πŸ“Š Architecture Decision Points | Decision | Current Approach | Why | |----------|------------------|-----| | **Caching** | Redis + cache-aside | TTL-based expiry is simple & performant | | **Cache Invalidation** | Prefix-based SCAN | Non-blocking, doesn't require key enumeration | | **Cache Metadata** | AsyncLocalStorage + Interceptor | Per-request context without global state | | **Query Patterns** | CQRS with QueryBus | Separates reads from writes, enables caching layer | | **Rate Limiting** | EndpointRateLimitGuard | Per-endpoint control, different rates for different ops | | **Quota Metering** | @RequireQuota decorator | Subscription-aware, tracks usage | | **Response Format** | DTO with cache metadata | Frontend knows freshness of data | | **Error Handling** | DomainException + InternalServerError | Differentiates logic errors from system errors | | **Graceful Degradation** | Cache bypass if Redis down | Service stays up during Redis maintenance | --- ## 🎯 Core Conventions to Remember βœ… **Always cache with TTL** - Dashboard tiles: 300s - Aggregations: 300s - Reports: 900s - Trends: 1800s - Predictions: NO CACHE (always fresh) βœ… **Always use CacheService.buildKey()** - Ensures deterministic, lowercase keys - Replaces spaces with underscores - Format: `prefix:param1:param2:param3` βœ… **Always wrap handlers in try-catch** ```ts try { // logic } catch (error) { if (error instanceof DomainException) throw error; this.logger.error(...); throw new InternalServerErrorException('...'); } ``` βœ… **Always return DTO with null metadata** ```ts return { // ...data fields cachedAt: null, // Filled by CacheMetaInterceptor nextRefreshAt: null, // Filled by CacheMetaInterceptor }; ``` βœ… **Always use @UseInterceptors(CacheMetaInterceptor)** - On controllers to wrap response - Adds: `{ data: T, cacheMeta: { cachedAt, nextRefreshAt, source } }` βœ… **Always add guards to endpoints** ```ts @UseGuards(JwtAuthGuard, QuotaGuard) // Auth + quota check @RequireQuota('analytics_queries') // Meter usage @EndpointRateLimit({ limit: 10, windowSeconds: 60 }) // Rate limit if needed ``` --- ## πŸ“ File Summary | File Path | Purpose | Lines | |-----------|---------|-------| | `presentation/controllers/analytics.controller.ts` | Main endpoints | 331 | | `presentation/controllers/avm.controller.ts` | Valuation endpoints | 171 | | `application/queries/*/get-*.handler.ts` | Query execution + caching | 50-100 ea | | `domain/repositories/market-index.repository.ts` | Repository interface | 58 | | `infrastructure/repositories/prisma-market-index.repository.ts` | Prisma implementation | 150+ | | `presentation/interceptors/cache-meta.interceptor.ts` | Response wrapper | 61 | | `../shared/infrastructure/cache.service.ts` | Redis cache layer | 191 | | `../shared/infrastructure/decorators/cacheable.decorator.ts` | @Cacheable | 57 | | `analytics.module.ts` | NestJS module definition | 102 | --- ## πŸŽ“ Next Steps 1. **Read** β†’ Start with `quick_reference.md` for visual understanding 2. **Reference** β†’ Use `file_paths_reference.md` to find specific files 3. **Deep Dive** β†’ Study `analytics_architecture_guide.md` for patterns & code 4. **Build** β†’ Follow the 7-step checklist to add your first endpoint 5. **Test** β†’ Create query handler spec following existing patterns --- ## Questions to Validate Understanding After reading the guides, you should be able to answer: 1. What are the 4 DDD layers and what goes in each? 2. How does the cache-aside pattern work when Redis is down? 3. What's the difference between @Cacheable and cache.getOrSet()? 4. Why do response DTOs have `cachedAt: null` and `nextRefreshAt: null`? 5. How is the cache key built deterministically? 6. What TTLs are used for different endpoint types? 7. What guards are required on all analytics endpoints? 8. How do you add a new GET endpoint in 7 steps? 9. What's the purpose of CacheMetaInterceptor? 10. How does graceful degradation work if Redis is unavailable? ---