- 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>
12 KiB
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:
-
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
-
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
-
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:
ListingDetailDatawith 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.qualityScorefield (denormalized aggregate) - Update trigger: Review and inquiry events
- Result: Score rounded to 1 decimal, displayed in agent profiles
5. Inquiries Tracking
- Creation:
InquiryEntitypersisted,InquiryCreatedEventpublished - Denormalization: Event listener increments
Listing.inquiryCount - Display:
inquiryCountreturned inListingDetailData - 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
{
"__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
-
Cache invalidation on null: Not caching null results is smart but requires careful coordination—ensure all listing creation/updates invalidate appropriately.
-
PostGIS geometry extraction: Always requires raw SQL queries (
$queryRaw) for lat/lng extraction. Prisma's ORM doesn't map PostGIS types directly. -
Denormalized inquiryCount: May lag behind actual count if event listener fails. Should have reconciliation job.
-
AVM service downtime: Falls back gracefully, but confidence score from fallback service may be lower. Frontend should handle confidence thresholds.
-
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.
-
Agent quality score with no reviews: Defaults to 50 (neutral). May want business logic to penalize agents with 0 reviews.
-
Cache stampede potential: If Redis is slow, multiple concurrent requests could trigger parallel loader calls. Implement request coalescing if needed.
📈 Recommended Next Steps
-
Verify inquiryCount event listener: Check listings module for event handler that increments inquiryCount on
InquiryCreatedEvent. -
Audit cache invalidation: Ensure all listing mutations (status change, price update, media upload) invalidate cache appropriately.
-
Test AVM fallback: Simulate Python service downtime; verify graceful fallback to PostGIS.
-
Performance review: Profile queries with EXPLAIN ANALYZE:
- Listing detail with media fetch
- Similar listings (price/area bounds)
- Inquiry count aggregation
- Agent quality score recalculation
-
Document cache TTL rationale: Why 300s for listing detail but only 120s for search? May need adjustment based on data freshness requirements.
-
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