A production-grade NestJS API for property listings with advanced filtering, cursor-based pagination, Redis caching with cache-aside pattern, and cache stampede prevention.
- Node.js 22+
- Docker & Docker Compose
- Postgres 18
- Redis 7
docker compose up --buildDocker Compose orchestrates the startup sequence:
- postgres and redis start first (with health checks)
- migrate runs database migrations and exits
- seed populates 100,000 property records and exits
- app starts only after all steps complete
API available at http://localhost:3000/v1. Swagger docs at http://localhost:3000/docs.
- Adapter Pattern —
CacheAdapterinterface abstracts Redis operations, making it trivial to swap the caching backend without touching business logic - Proxy Pattern —
CacheProxywraps all cache operations with timing metrics, hit/miss logging, and sanitized error handling that prevents sensitive data leaks - Facade Pattern —
RedisServiceprovides a clean, typed API for cache operations so consumers don't deal with raw cache managers or key building - Cache-Aside Pattern — Checks cache first, falls back to database on miss, stores result for future requests
- Cache Stampede Prevention — Distributed lock (
CacheLock) deduplicates concurrent requests for the same cache key. Only one request hits the database while others wait and share the result
- Cursor-based pagination instead of offset pagination — avoids slow
OFFSETqueries on large datasets - UUIDv7 IDs — time-sortable UUIDs used for primary keys, enabling stable pagination without secondary index lookups. The query uses
ORDER BY price DESC, id DESCwhere UUIDv7 guarantees that records with the same price are returned in insertion order - Response includes
hasMoreandnextCursorfor reliable navigation
- 6 partial indexes on active records only — reduces index size and query time
- GIN trigram index on
citycolumn — enables fastILIKE '%pattern%'substring searches without full table scans - Postgres 18 with io_uring support — modern async I/O for better throughput
- Redis caching with configurable TTL (5 min default) — reduces database load for repeated queries
- Input validation with
class-validator—@Max(100)onlimitprevents DB hammering,@MaxLength(100)oncityprevents oversized cache keys (memory DoS) - Error sanitization — logs only
error.message, never full error objects that may contain connection strings or stack traces - Scoped cache invalidation — tracks and deletes only property cache keys, never wipes all Redis data
- Parameterized queries — all filter values use TypeORM's parameterized binding, preventing SQL injection
- UUIDv7 validation — cursor parameter validated as proper UUIDv7 format
- API versioning —
/v1/prefix for backward compatibility
- Unit tests (31) — Service, Controller, Filter utils, CacheLock
- E2E tests (3) — Full request pipeline with mocked dependencies
- k6 load tests — Validates performance under 50 concurrent users with pass/fail thresholds
- GitHub Actions CI — Runs build, unit tests, and E2E tests on every push/PR with Postgres and Redis service containers
- Multi-stage Dockerfile — optimized build with minimal production image
- Docker Compose — 5-service orchestration: Postgres, Redis, migrate (one-shot), seed (one-shot), app
- Health checks on all services — app only starts after seed completes
- No persistent volumes — clean state on restart, ideal for development
Tested locally with k6 (50 virtual users, 50 seconds, 100K records in database):
execution: local
script: load-test.js
scenarios: (100.00%) 1 scenario, 50 max VUs, 50s duration
| Metric | Result |
|---|---|
| Total Requests | 2,777 |
| Throughput | 55.4 req/s |
| p(95) Latency | 6.58ms |
| p(90) Latency | 5.35ms |
| Median | 3.57ms |
| Min | 1.44ms |
| Max | 51.66ms |
| Error Rate | 0.00% |
| All Checks Passed | 100% (8,331/8,331) |
0-2ms ████ 5% of requests
2-5ms ████████████████████ 75% of requests (median: 3.57ms)
5-10ms ████████ 15% of requests
10-20ms ██ 3% of requests
20-50ms █ 2% of requests
█ THRESHOLDS
errors
✓ 'rate<0.1' rate=0.00%
http_req_duration
✓ 'p(95)<500' p(95)=6.58ms
50 ┤ ┌─────────────── 50 users
│ ╱│
40 ┤ ╱ │
│ ╱ │
30 ┤ ╱ │
│ ╱ │
20 ┤──────╱─────────────┘ │
│ ╱ │
10 ┤──╱ │
│╱ │
0 ┼─────────────────────────────────────→ Time (50s)
0s 10s 30s 50s
Ramp Up Steady Peak Ramp Down
Load Load
The cache-aside pattern means only the first request for each unique filter combination hits the database. Subsequent requests are served from Redis in <5ms:
Cache MISS ──→ App ──→ Database ──→ Redis ──→ Response (15-50ms)
│
Cache HIT ──→ App ──→ Redis ────────┘ (1-5ms)
With 100,000 records and complex filters, raw database queries take 50-200ms. Redis caching reduces this to single-digit milliseconds.
Note: These results are from a single local development machine. In a production cloud environment with dedicated resources (ElastiCache, RDS, horizontal scaling), these metrics will improve significantly.
Retrieve paginated property listings with filters.
| Parameter | Type | Description |
|---|---|---|
| minPrice | number | Minimum price |
| maxPrice | number | Maximum price |
| city | string | City name (case-insensitive substring match, max 100 chars) |
| minBedrooms | number | Minimum bedrooms |
| minAreaSqm | number | Minimum area in m² |
| maxAreaSqm | number | Maximum area in m² |
| isAvailable | boolean | Availability filter |
| operator | string | AND or OR (default: AND) |
| cursor | string | UUIDv7 pagination cursor |
| limit | number | Items per page (1-100) |
Health check endpoint for Docker healthchecks and load balancers.
{
"data": [
{
"id": "019ddfe0-0000-0000-0000-000000000000",
"title": "Modern Apartment",
"price": 450000,
"city": "Houston",
"address": "123 Main St",
"bedrooms": 3,
"bathrooms": 2,
"areaSqm": 120,
"isAvailable": true,
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}
],
"meta": {
"hasMore": true,
"nextCursor": "019ddfe0-0005-0000-0000-000000000005",
"limit": 10
}
}src/
├── cache/
│ ├── adapters/ # CacheAdapter implementation
│ ├── interfaces/ # Cache contract
│ ├── proxy/ # CacheProxy with logging
│ ├── cache-lock.ts # Stampede prevention
│ ├── cache-key.builder.ts
│ ├── redis.module.ts
│ └── redis.service.ts # Facade for cache operations
├── property/
│ ├── dtos/
│ ├── entities/
│ ├── property.service.ts
│ ├── property.controller.ts
│ └── filters.utils.ts
├── database/
│ └── database.module.ts
└── main.ts
migrations/
seeds/
test/
├── unit/
└── e2e/
npm run build # Build the application
npm run start:dev # Start in watch mode
npm run migration:run # Run migrations
npm run migration:revert # Revert last migration
npm run seed # Seed 100,000 property records
npm run test:unit # Run unit tests
npm run test:e2e # Run E2E tests
npm run lint # Fix ESLint issues
npm run format # Format with Prettier
k6 run load-test.js # Run load testing