~80% of queries hit the fast lane
Intelligent restaurant search
Conversational discovery with explicit routing, predictable AI unit economics, and latency targets suitable for high-volume production traffic.
Multi-tenant microservice · Tiered search · Observable by design
Executive summary
We delivered a multi-tenant restaurant discovery microservice that resolves natural-language queries (for example, “cozy Italian place for date night”) while keeping the majority of requests on a sub-100ms path. The architecture caps model spend at roughly $0.001 per AI-assisted query on average, combines Redis caching with deterministic lane selection, and ships with the metrics, logging, and guardrails operators expect in production.

Service commitments
Latency, cost, isolation, and operations
Budgeted hybrid retrieval
Isolated rows per tenant
Metrics, logs, safe limits
Three-lane search architecture
Each query is classified before any paid inference: deterministic paths carry the bulk of volume; models run only when phrasing requires interpretation; a short-TTL cache reduces repeat load on the database and providers.
Keyword search
Simple intents, zero AI spend.
- Queries like “Italian”, “sushi”, “pizza”.
- PostgreSQL ILIKE plus PostGIS geo filters.
- Predictable path—no model calls.
$0 marginal cost per query
Natural language AI search
Complex intent → structured retrieval.
- Examples: “romantic spot with live music near me”.
- Claude Haiku parses intent to structured JSON.
- OpenAI embeddings + pgvector cosine similarity in Postgres.
~$0.001 per search (budgeted)
Result cache
Repeat demand disappears at the edge.
- Redis with 30s TTL on hashed keys.
- Key = tenant + query + geo + filters + classification.
- Hit-rate goal 30–40% to protect DB and models.
Technical stack
Implementation stack
Backend
- NestJS 10
- TypeScript (strict)
- DDD + Hexagonal + CQRS
Data
- PostgreSQL 17
- PostGIS
- pgvector
- Row-level security (tenancy)
Cache & realtime
- Redis 7 — result cache (30s)
- Semantic intent cache (60s)
- LLM rate limits (100/min/tenant)
- Pulse: WebSocket + Redis pub/sub
AI services
- Claude Haiku 4.5 — intent parsing (~90% of AI calls)
- Claude Sonnet 4.6 — menu Q&A only
- OpenAI text-embedding-3-small (1536-d)
Bounded contexts
Partitioning for maintainable evolution
Five bounded contexts share a small kernel so search behavior can evolve without coupling unrelated concerns or destabilizing shared infrastructure.
Primary bounded context
Discovery
The search core
- Query classifier picks the lane.
- Intent parser (Claude) for NL queries.
- Search index in Postgres + hybrid ranker.
Engineering patterns
- Domain-Driven Design
- Hexagonal architecture
- CQRS
- Event-driven architecture
Tenancy
Configuration per brand
- Valid vibes, cuisines, ranker weights, embedding templates.
- In-memory registry with Redis warmup.
Indexing
Fresh vectors & listings
- Venue CRUD on the search index.
- Embedding saga on EntityIndexedEvent.
GoldenKeys
Smart filter chips
- Auto-generated chips like “Rooftop”, “Live music”.
- Cron every 6h from search analytics.
Pulse
Live venue status
- Open/busy/wait signals.
- Gateway on /pulse with WebSocket fan-out.
Shared kernel
- TenantId & Geo value objects
- Result<T, E> for explicit failures
- Prisma + Redis services
- TenantContext via AsyncLocalStorage
- Pino logging
- Claude & OpenAI adapters