# Listings Module Exploration - Summary **Exploration Date:** April 21, 2026 **Scope:** Complete understanding of listings, properties, AVM, agents, inquiries, and caching ## πŸ“‹ Documents Generated This exploration has produced **3 comprehensive reference documents**: 1. **LISTINGS_MODULE_EXPLORATION.md** (965 lines) - Detailed, section-by-section breakdown of all 7 components - Code snippets from actual source files - Full interface definitions and data flow diagrams 2. **LISTINGS_QUICK_REFERENCE.md** (Quick lookup) - Visual flow diagrams using ASCII art - 1-page reference for each major component - Key formulas, algorithms, and cache configuration 3. **LISTINGS_DATA_SCHEMA.md** (Database focus) - Table definitions in SQL - Foreign key relationships - Index strategy and query patterns - Denormalization strategy & eventual consistency model --- ## 🎯 Key Findings ### 1. GET /listings/:id Architecture - **Handler:** `ListingsController.getListing()` β†’ `GetListingQuery` β†’ `GetListingHandler` - **Cache:** Redis cache-aside, 300s TTL, envelope-based storage with metadata - **Not-found behavior:** Uses internal signal to avoid caching null; allows new listings to be discoverable immediately - **Response:** `ListingDetailData` with nested property, seller, agent info ### 2. Response DTO (ListingDetailData) - Contains **full property details** (40+ fields) - Includes **engagement metrics**: `viewCount`, `saveCount`, **`inquiryCount`** (denormalized) - Includes **featured status**: `isFeatured`, `featuredUntil` (computed at runtime) - Media: Up to 10 images/videos with order & captions - Seller & agent info with contact details ### 3. AVM Service Integration - **Primary:** Python AI microservice (v1 or v2 model) - **Fallback:** PostGIS-based comparables if AI service down - **Batch concurrency:** Max 5 concurrent requests - **Input:** Property attributes (area, district, bedrooms, etc.) - **Output:** Estimated price + confidence + comparables + model version ### 4. Agent Quality Score - **Formula:** Weighted average of 4 metrics (40% reviews, 30% response time, 20% conversion, 10% listing activity) - **Storage:** `Agent.qualityScore` field (denormalized aggregate) - **Update trigger:** Review and inquiry events - **Result:** Score rounded to 1 decimal, displayed in agent profiles ### 5. Inquiries Tracking - **Creation:** `InquiryEntity` persisted, `InquiryCreatedEvent` published - **Denormalization:** Event listener increments `Listing.inquiryCount` - **Display:** `inquiryCount` returned in `ListingDetailData` - **Data:** Inquirer, message (HTML-sanitized), optional phone, read status, timestamp ### 6. Similar Listings Algorithm - **Rule-based matcher** (not ML-based) - **Criteria:** Same property type & district, price Β±10%, area Β±20%, ACTIVE status - **Sorting:** By price delta (closest comparable first) - **Performance:** Fetches 3x limit, sorts by delta, returns top N ### 7. Redis Caching - **Pattern:** Cache-aside with envelope metadata - **TTLs:** 300s (listing detail), 120s (search), 60s (quota), 86400s (reference data) - **Graceful degradation:** System works offline; metrics track all failures - **Invalidation:** Single key or prefix-based (SCAN-based for prefix) - **Metrics:** Hit/miss/degradation counters by resource --- ## πŸ—‚οΈ File Organization ### Listings Module (`apps/api/src/modules/listings/`) ``` listings/ β”œβ”€β”€ presentation/ β”‚ β”œβ”€β”€ controllers/listings.controller.ts ← HTTP handler β”‚ └── dto/ ← Request/response DTOs β”œβ”€β”€ application/ β”‚ β”œβ”€β”€ queries/get-listing/ ← Query handler + cache logic β”‚ └── commands/ ← Create, update, delete β”œβ”€β”€ domain/ β”‚ β”œβ”€β”€ entities/listing.entity.ts ← Domain model β”‚ β”œβ”€β”€ repositories/listing-read.dto.ts ← Response DTOs β”‚ └── events/ ← Domain events └── infrastructure/ β”œβ”€β”€ repositories/listing-read.queries.ts ← SQL queries (including similar listings) └── services/ ``` ### Analytics Module (`apps/api/src/modules/analytics/`) ``` analytics/ β”œβ”€β”€ domain/services/avm-service.ts ← AVM interface └── infrastructure/services/ β”œβ”€β”€ http-avm.service.ts ← Primary implementation β”œβ”€β”€ ai-service.client.ts ← AI microservice HTTP client └── prisma-avm.service.ts ← Fallback (PostGIS) ``` ### Agents Module (`apps/api/src/modules/agents/`) ``` agents/ β”œβ”€β”€ domain/services/quality-score.service.ts ← Calculator (pure domain) β”œβ”€β”€ application/commands/recalculate-quality-score/ └── infrastructure/repositories/agent-profile.queries.ts ``` ### Inquiries Module (`apps/api/src/modules/inquiries/`) ``` inquiries/ β”œβ”€β”€ application/commands/create-inquiry/ ← Creates + publishes event β”œβ”€β”€ domain/entities/inquiry.entity.ts └── domain/events/inquiry-created.event.ts ``` ### Cache Service (`apps/api/src/modules/shared/`) ``` shared/ └── infrastructure/cache.service.ts ← Cache-aside implementation ``` --- ## πŸ”— Data Flow Diagram ``` Client HTTP Request: GET /listings/{id} ↓ ListingsController.getListing(id) ↓ GetListingHandler.execute(GetListingQuery) β”œβ”€ CacheService.getOrSet(cache:listing:{id}, loader, 300s) β”‚ β”œβ”€ Redis hit? β†’ return ListingDetailData β”‚ β”œβ”€ Redis miss β†’ call loader() β”‚ β”‚ β”œβ”€ prisma.listing.findUnique({id}, include: {...}) β”‚ β”‚ β”œβ”€ Extract PostGIS geometry (lat/lng) β”‚ β”‚ β”œβ”€ Map to ListingDetailData β”‚ β”‚ └─ Store in Redis (envelope + metadata) β”‚ └─ Not found? β†’ throw ListingNotFoundSignal (don't cache) β”œβ”€ Exception handling └─ Return ListingDetailData | null ↓ Controller: null β†’ throw NotFoundException (404) ↓ Response: 200 OK + JSON(ListingDetailData) ``` --- ## πŸ“Š Component Dependencies ``` ListingsController β”œβ”€ CommandBus (for mutations) └─ QueryBus (for queries) β”œβ”€ GetListingHandler β”‚ β”œβ”€ ListingRepository β”‚ β”œβ”€ CacheService β”‚ └─ LoggerService β”œβ”€ GetSimilarListingsHandler β”‚ └─ ListingRepository β”‚ └─ Prisma (for SQL queries) └─ SearchListingsHandler └─ ListingRepository β”œβ”€ Prisma └─ PostGIS (geometry extraction) AgentsModule β”œβ”€ QualityScoreCalculator (pure domain service) β”œβ”€ RecalculateQualityScoreHandler β”‚ └─ Triggered by ReviewCreatedEvent, InquiryCreatedEvent └─ AgentProfileQueries β”œβ”€ Prisma (fetch agent + reviews + listings) └─ ReviewAggregation InquiriesModule β”œβ”€ CreateInquiryHandler β”‚ β”œβ”€ InquiryRepository β”‚ β”œβ”€ EventBus β”‚ └─ InquiryCreatedEvent (published) └─ ListingModule receives event └─ Increments Listing.inquiryCount AnalyticsModule (AVM) β”œβ”€ HttpAVMService β”‚ β”œβ”€ IAiServiceClient (HTTP to Python service) β”‚ β”œβ”€ PrismaAVMService (fallback) β”‚ └─ Prometheus metrics └─ Used by: Listing valuation, Comparable search SharedModule (Cache) β”œβ”€ CacheService β”‚ β”œβ”€ RedisService β”‚ └─ Metrics (hit/miss/degradation counters) └─ Used by: GetListingHandler, SearchListingsHandler, etc. ``` --- ## πŸ› οΈ Important Implementation Details ### Cache Envelope Format ```json { "__v": { /* actual data */ }, "cachedAt": "2026-04-21T12:00:00Z", "ttlSeconds": 300 } ``` - Allows frontend to display cache freshness - Backward-compatible with legacy plain-JSON entries - Metadata stored in async storage (`cacheMetaStorage`) ### Denormalization Pattern ``` Event Publish (InquiryCreatedEvent) ↓ Event Listener (in Listings or Inquiry module) β”œβ”€ Count inquiries for listing: β”‚ await prisma.inquiry.count({ where: { listingId } }) β”œβ”€ Update listing counter: β”‚ await prisma.listing.update({ β”‚ where: { id: listingId }, β”‚ data: { inquiryCount: count } β”‚ }) └─ Cache invalidation (optional): cache.invalidate(cache:listing:{id}) ``` - Eventual consistency (may lag seconds) - Avoids expensive COUNT() on every read - Event-driven guarantees eventual correctness ### AVM Fallback Chain ``` estimateValue(params) β”œβ”€ Try: aiClient.predict(AiPredictRequest) β”‚ β†’ HTTP POST to Python service β”‚ β†’ Return AI-based ValuationResult └─ Catch (any error): └─ fallback.estimateValue(params) β†’ PostGIS spatial query β†’ Find comparable sales nearby β†’ Calculate median price β†’ Return comparables-based ValuationResult ``` ### Quality Score Formula ``` Score = (avgRating / 5) * 100 * 0.40 + MAX(0, 100 - (responseTimeAvg / 3600) * 100) * 0.30 + (conversionRate * 100) * 0.20 + (activeListingRatio * 100) * 0.10 Rounded to 1 decimal place ``` --- ## ⚠️ Potential Issues & Edge Cases 1. **Cache invalidation on null:** Not caching null results is smart but requires careful coordinationβ€”ensure all listing creation/updates invalidate appropriately. 2. **PostGIS geometry extraction:** Always requires raw SQL queries (`$queryRaw`) for lat/lng extraction. Prisma's ORM doesn't map PostGIS types directly. 3. **Denormalized inquiryCount:** May lag behind actual count if event listener fails. Should have reconciliation job. 4. **AVM service downtime:** Falls back gracefully, but confidence score from fallback service may be lower. Frontend should handle confidence thresholds. 5. **Similar listings boundary cases:** Price Β±10% is exact mathematical boundaryβ€”no tolerance. Two listings with 10.1% price difference won't be marked as similar. 6. **Agent quality score with no reviews:** Defaults to 50 (neutral). May want business logic to penalize agents with 0 reviews. 7. **Cache stampede potential:** If Redis is slow, multiple concurrent requests could trigger parallel loader calls. Implement request coalescing if needed. --- ## πŸ“ˆ Recommended Next Steps 1. **Verify inquiryCount event listener:** Check listings module for event handler that increments inquiryCount on `InquiryCreatedEvent`. 2. **Audit cache invalidation:** Ensure all listing mutations (status change, price update, media upload) invalidate cache appropriately. 3. **Test AVM fallback:** Simulate Python service downtime; verify graceful fallback to PostGIS. 4. **Performance review:** Profile queries with EXPLAIN ANALYZE: - Listing detail with media fetch - Similar listings (price/area bounds) - Inquiry count aggregation - Agent quality score recalculation 5. **Document cache TTL rationale:** Why 300s for listing detail but only 120s for search? May need adjustment based on data freshness requirements. 6. **Implement reconciliation job:** Periodic job to recount inquiries per listing, detect denormalization drift. --- ## πŸ“– References **All source files are organized in the 3 generated documents:** - Detailed exploration: `LISTINGS_MODULE_EXPLORATION.md` - Quick reference: `LISTINGS_QUICK_REFERENCE.md` - Database schema: `LISTINGS_DATA_SCHEMA.md` **Key base paths:** - Listings module: `apps/api/src/modules/listings/` - Analytics (AVM): `apps/api/src/modules/analytics/` - Agents: `apps/api/src/modules/agents/` - Inquiries: `apps/api/src/modules/inquiries/` - Cache service: `apps/api/src/modules/shared/infrastructure/cache.service.ts` --- **End of Exploration Summary**