Cosmic Module
D
Qubits of DPK
April 11, 2026
Core DBMS
Domain Understanding
Zomato is a food delivery platform connecting three entities: customers who order food, restaurants that prepare it, and delivery partners who bring it. The schema must handle the complete order lifecycle from cart to delivery, real-time location tracking, dynamic pricing, and the restaurant catalogue (menus with items and variants). It is one of the most interview-relevant schemas because it touches almost every schema design concept: self-referential relationships, soft deletes, state machines, denormalization, audit fields, and high-concurrency writes.
The read-to-write ratio is mixed. Menu reads are very frequent. Order writes are high-volume during meal hours. Location updates from delivery partners are extremely write-heavy (GPS ping every few seconds).
Entity Map
sql
QUBITS OF DPK
Full Schema with Comments
sql
QUBITS OF DPK
Key Design Decisions
The Price Snapshot Problem
This is the most commonly missed design insight in e-commerce and food delivery schemas.
If order_items only stores item_id and quantity, what happens when the restaurant changes Butter Chicken from ₹250 to ₹290 next month? Every historical order that looks up menu_items.price via the FK now shows the new price. The customer's receipt is wrong. The restaurant's revenue reconciliation is wrong.
The solution is to snapshot the price and name at order creation time into order_items.unit_price and order_items.item_name. These columns are set once when the order is placed and never updated. The FK to menu_items is kept for catalogue reference, but the price and name for billing purposes come from the snapshot columns.
This is an intentional, justified denormalization. You are trading storage (a few extra columns per order item) for temporal correctness (accurate historical billing records).
Indexing Strategy
Common Traps to Avoid
- Not snapshotting price and name in order_items — Price changes corrupt historical order records. Always snapshot at order time.
- Storing only current status on orders, no history table — You lose the ability to measure time-between-states (how long did preparation take?) and cannot debug delivery disputes.
- Computing avg_rating live on listing pages — Aggregating reviews for 500 restaurants on every search query will kill the database. Precompute and denormalize.
- Not having a separate location_updates table — Updating current_lat/lng on delivery_partners for every GPS ping creates massive write contention on a single row per partner. Append to location_updates instead and denormalize only the latest position.
- Hard-deleting menu items — Order history references menu items. Hard delete breaks FK integrity and historical records.
Interview Q&A
Q1: Why does order_items store item_name and unit_price if there's already a FK to menu_items?
Because menu items change over time. A restaurant can rename a dish or change its price. The FK preserves the catalogue relationship, but billing records must reflect the price at the time of purchase. unit_price and item_name are snapshots taken at order creation and never modified afterward.
Q2: How do you track the full lifecycle of an order?
Two mechanisms: orders.order_status holds the current state for fast reads. order_status_history is an append-only table that records every status transition with a timestamp and actor. This enables SLA tracking (how long was the order in PREPARING state?), support queries, and dispute resolution.
Q3: How do you find the nearest available delivery partner efficiently?
delivery_partners has a denormalized current_lat and current_lng updated after each location ping. The nearest-partner query runs a haversine distance calculation against rows where is_online = TRUE. At production scale this uses a geospatial index (MySQL SPATIAL index or PostGIS) or a Redis Geo sorted set for sub-millisecond lookups.
Q4: Why is location_updates a separate table instead of updating a column on delivery_partners?
GPS pings arrive every 5 seconds per partner. Updating the same row on delivery_partners 720 times per hour per partner creates severe write contention and InnoDB row lock conflicts. Appending to location_updates is a pure INSERT with no locking conflicts. The current_lat/lng on delivery_partners is updated asynchronously by a background job as a read optimisation.
Q5: How do you handle a refund when an order is cancelled?
The payments table has a status column with a REFUNDED state. A refund creates a new entry in a payment_refunds table (not shown here for brevity) linked to the original payment, recording the refund amount, reason, and gateway transaction ID. The original payment row is never modified.
ANKI Cards
Q: Why are item_name and unit_price stored as snapshot columns in order_items instead of always reading from menu_items?
A: Menu item names and prices change over time. Snapshotting at order creation time ensures historical orders always reflect the price and name at the time of purchase, regardless of future menu changes. The FK to menu_items is kept for catalogue reference only.
Q: In a food delivery schema, why is there both orders.order_status AND an order_status_history table?
A: order_status is the current state — fast to read for active order tracking. order_status_history is the append-only audit trail recording every transition with a timestamp. You need both: current state for the app UI, full history for SLA measurement and dispute resolution.
Q: Why is DECIMAL(9,6) used for latitude/longitude instead of FLOAT?
A: FLOAT has only ~7 significant digits of precision. At the equator, accurate geolocation to within 11 centimetres requires 6 decimal places. DECIMAL(9,6) stores exact values with 6 decimal places of precision. FLOAT accumulates rounding errors that corrupt precise location matching.