The Problem: Legacy POS Systems Can't Scale
When Dineezy approached us, they were running a modified off-the-shelf POS across 50+ restaurant outlets. Peak hour meant order delays, lost tickets, and frustrated staff. The existing system polled the server every 30 seconds — an eternity in a busy kitchen.
We needed a system that could handle 10,000+ daily orders with instant propagation from counter to kitchen to delivery.
Architecture Decision: Event-Driven Over Polling
The first architectural decision was moving from HTTP polling to WebSocket-based event streams. Every order state change (placed → confirmed → preparing → ready → delivered) propagates in real-time to all connected terminals.
Why Not Server-Sent Events?
SSE is simpler but uni-directional. Our kitchen display systems need to send acknowledgments back ("order picked up", "prep started"), making WebSockets the right choice.
The Tech Stack
| Layer | Technology | Why |
|---|---|---|
| API Gateway | Node.js + Fastify | Low overhead, schema validation |
| Real-time | WebSocket (ws) + Redis Pub/Sub | Cross-instance message fanout |
| Database | PostgreSQL + TimescaleDB | Time-series order analytics |
| Queue | BullMQ on Redis | Async jobs (receipts, notifications) |
| Frontend | Next.js + React | SSR for admin, CSR for terminals |
Real-Time Message Flow
When a customer places an order at the counter:
- Counter terminal sends order via WebSocket
- API validates and persists to PostgreSQL
- Redis Pub/Sub broadcasts the event to all connected instances
- Kitchen Display receives the order instantly (<100ms)
- BullMQ queues async tasks — receipt printing, SMS to customer
Handling 50+ Outlets Simultaneously
Each outlet maintains its own WebSocket connection pool. We use Redis Pub/Sub channels per outlet so kitchen staff only see their orders:
- Channel:
orders:{outletId}:new - Channel:
orders:{outletId}:status - Channel:
orders:{outletId}:cancel
This keeps message volume manageable — a single outlet processes ~200 orders during peak hour, not 10,000.
Results After 3 Months
The production deployment showed dramatic improvements:
- Order latency: 30s polling → 87ms average WebSocket delivery
- Lost orders: 12/day → 0 (zero lost tickets in 90 days)
- Kitchen throughput: 15% faster prep times (instant visibility)
- System uptime: 99.97% over 90 days
Key Lesson: Don't Over-Engineer Early
We initially planned for Kafka-based event sourcing. After load testing, Redis Pub/Sub handled 50 outlets with headroom to spare. We saved 3 weeks of development and $400/month in infrastructure costs by choosing the simpler path first.
"The best architecture is the simplest one that solves the problem at current scale, with a clear upgrade path for the next order of magnitude."
