Case study · Systems architecture

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.

Case study visual for AI-assisted restaurant search and discovery product
Case study visual for AI-assisted restaurant search and discovery product
System overviewProduction posture

Service commitments

Latency, cost, isolation, and operations

<100msSearch SLA

~80% of queries hit the fast lane

~$0.001Avg. cost / AI search

Budgeted hybrid retrieval

Multi-tenantPostgres RLS

Isolated rows per tenant

ObservableProduction-ready

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.

1~80% of traffic · <80ms target

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

2~20% of traffic · <1200ms target

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)

3Hot paths · <15ms target

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