Files
goodgo-platform/docs/explorations/from-desktop/00_SUMMARY.md
Ho Ngoc Hai 08b96f9c2d docs: consolidate exploration & audit reports under docs/ (TEC-3094)
- Move 8 stray .md (+5 .txt) from ~/Desktop into docs/explorations/from-desktop/
- Reorganize 27 .md/.txt at workspace root:
  - audit reports -> docs/audits/
  - exploration reports -> docs/explorations/
  - design system -> docs/design-system/
- Keep only README/CHANGELOG/CONTRIBUTING/CLAUDE at repo root
- Refresh docs/README.md as canonical index with links to all groups
- Note: pre-existing docs/audits/AUDIT_INDEX.md and AUDIT_SUMMARY.md were
  overwritten by the newer root-level versions during the move

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 16:29:24 +07:00

11 KiB
Raw Blame History

🎯 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)

@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<DistrictStatsDto> {
    return this.marketIndexRepo.getDistrictStats(query.city, query.period);
  }
}

Pattern 2: cache.getOrSet() (For complex computation)

async execute(query): Promise<MarketSnapshotDto> {
  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)

    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)

    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)

    @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<GetTrendingAreasDto> {
        // 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)

    const QueryHandlers = [
      // ... existing
      GetTrendingAreasHandler,  // Add here
    ];
    
  5. Controller Method (in analytics.controller.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<GetTrendingAreasDto> {
      return this.queryBus.execute(
        new GetTrendingAreasQuery(dto.city || 'Hồ Chí Minh', dto.propertyType, dto.limit || 10),
      );
    }
    
  6. Export DTOs (in presentation/dto/index.ts)

    export * from './get-trending-areas.dto';
    
  7. Test (in __tests__/get-trending-areas.handler.spec.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

try {
  // logic
} catch (error) {
  if (error instanceof DomainException) throw error;
  this.logger.error(...);
  throw new InternalServerErrorException('...');
}

Always return DTO with null metadata

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

@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?