Design problems
Maps, mobility & logisticsMediumSDE-2 35 min

Design shipment tracking for a logistics company (Delhivery)

Asked at
AmazonDelhiveryFlipkartMeeshoSwiggy

The interviewer asks

“Design shipment tracking for a logistics company like Delhivery. Every parcel is scanned at pickup, at each hub and on the delivery van. Merchants and customers want to see where a parcel is, when it will arrive, and get notified when it moves.”

What a strong answer looks like

A strong answer models tracking as an append-only stream of scan events per shipment (the waybill, or AWB), and the visible status as a projection computed from that stream. Events come from handheld scanners and sorter machines in hubs that often have poor connectivity, so they arrive late, duplicated and out of order; the design must be idempotent (device-generated event IDs) and order-tolerant (apply by event time, never regress a terminal state). Ingestion goes through an API into Kafka partitioned by AWB; a projector updates the current-status store and the event history; downstream consumers send merchant webhooks and customer SMS/WhatsApp, and recompute the expected delivery date. Reads are heavy and bursty (customers refreshing during festive sales), so the tracking page is served from a cache keyed by AWB. Deep dives: offline-first scanning and out-of-order events, the status projection, notifications at scale, and expected-delivery-date prediction.

Concepts to mention

append-only scan events per AWBstatus as a projection (event sourcing)device-generated event IDs for idempotencyout-of-order events and terminal-state rulesKafka partitioned by AWBconditional update with versionoffline-first local queue on scannerssigned webhooks with retries and DLQCDN and Redis caching of trackingEDD prediction recomputed on events and timers

1 Requirements

Functional

  • Record scan events for a shipment: picked up, inbound/outbound at a hub, bagged, out for delivery, delivered, failed attempt, RTO (return to origin).
  • Show a tracking timeline and current status by AWB number, for customers (public, limited) and merchants (full detail).
  • Show an expected delivery date (EDD) and update it when delays happen.
  • Notify merchants by webhook and customers by SMS/WhatsApp/email on key status changes.
  • Show the live location of the delivery van or rider when the shipment is out for delivery.
  • Ops search: find shipments by merchant, hub, status and date for exceptions (stuck, misrouted).

Non-functional

  • Scale: ~3M shipments/day normally, 3x during festive sales; ~20 scan events per shipment.
  • Tracking page p95 under 300 ms; status visible within ~1 minute of a scan reaching our servers.
  • No lost scans: an event accepted by the device must eventually be recorded exactly once in effect.
  • Handheld devices may be offline for minutes to hours; ingestion must accept late and batched events.
  • Availability 99.95% for ingestion and tracking reads.
  • Keep event history for ~2 years for disputes and audit; hot data for ~90 days.

Out of scope (say it out loud)

  • Route planning and hub sortation logic (consume their outputs only).
  • Billing and cash-on-delivery remittance.
  • Warehouse management for fulfilment.

2 Estimates

Shipments~3M/day, ~9M/day festive peakLarge Indian 3PL scale; festive sales (Big Billion Days, Diwali) bring 2-3x.
Scan events~60M/day ≈ 700/s avg, ~5k/s peak3M × ~20 scans (pickup, 3-4 hubs × in/out/bag, OFD, delivery) = 60M/day; hub shifts bunch scans, so ×3 within the day, ×3 for festive.
Tracking reads~50M/day ≈ 600/s avg, ~10k/s peakCustomers check several times per order, merchants poll via API, marketplaces mirror status. Highly cacheable by AWB.
Webhooks to merchants~15M/day~5 milestone events per shipment × 3M; bursty after hub shift ends.
Rider GPS~10k/s~50k riders out for delivery × 1 ping per 5 s.
Event storage~30 GB/day, ~20 TB for 2 years60M × ~500 bytes (AWB, type, hub, device, timestamps, metadata); replication ×3 on the hot store, cold in S3 Parquet.

3 API

EndpointWhat it does
POST/v1/scans:batchDevice uploads a batch of scan events [{event_id (UUID from device), awb, type, location_id, device_ts, payload}]; returns per-event ack.
GET/v1/track/{awb}Public tracking: current status, masked timeline, EDD; cached.
GET/v1/merchant/shipments/{awb}Full detail for the owning merchant (auth by API key).
POST/v1/merchant/webhooksMerchant registers a webhook URL and event types; deliveries are signed.
GET/v1/ops/shipments?hub=&status=&since=Ops search for exceptions; served from Elasticsearch.
SSE/v1/track/{awb}/liveLive rider location and stop count while out for delivery.

4 Data model

scan_eventsCassandra (or ScyllaDB)
PK awbclustering (event_ts, event_id); typelocation_iddevice_idreceived_atpayload

Append-only history per shipment; partition by AWB keeps a shipment's ~20 events together. Deduplicate on event_id via lightweight insert or idempotent overwrite with same key. Exported daily to S3 Parquet.

shipment_statusDynamoDB or Postgres sharded by awb
awb (PK)merchant_idstatusstatus_tscurrent_locationnext_locationeddattemptsversionlast_event_id

The projection that tracking reads. Updated with conditional writes so an older event cannot overwrite a newer status.

shipments (manifest)Postgres
awbmerchant_idorigin_pindest_pinweightpayment_modecreated_atpromised_date

Created when the merchant manifests the order; the planned route lives here.

dedupe setRedis
seen:{event_id} with TTL 7 days

Fast duplicate check before writing; the conditional write on the status row is the final safety net.

ops indexElasticsearch
awbmerchanthubstatusage_in_statuspin codes

Fed from the status change stream; supports 'stuck at hub for > 24h' queries.

tracking cacheRedis + CDN
track:{awb} → rendered status JSONTTL ~60 s

Invalidated on status change; CDN caches the public response for 30-60 s.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALbatched scansacks after writeappend eventconditional updatededupe, invalidatestatus changesnew EDDtimelineops indexHub scanners / sortersCustomer / merchantAPI gateway + CDNTracking APIScan ingest APIEDD predictorStatus projectorNotifier + webhooksKafka (scans by AWB)Status storeCassandra (events)Redis (cache, dedupe)Elasticsearch (ops)SMS / WhatsApp /merchants
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

A parcel is scanned at a hub

  1. 1The handheld app writes the scan to a local SQLite queue with a UUID event_id and device timestamp, and shows success to the operator immediately.
  2. 2A background sync sends queued events in batches of up to ~200 to POST /scans:batch when the network is available.
  3. 3The ingest API validates schema and device auth, writes the batch to Kafka (topic scans, key = AWB, acks=all) and returns per-event acks; the device deletes acked events.
  4. 4The status projector consumes by partition, so events of one AWB are processed in order of arrival; it skips event_ids already seen.
  5. 5It appends the event to Cassandra, then computes the new status using event time and the state rules, and writes it with a conditional update on version.
  6. 6If the visible status changed, it invalidates track:{awb} and emits a StatusChanged event for notifications, EDD and the ops index.

Customer checks tracking

  1. 1Customer opens the tracking link; the CDN serves a cached response if it is under ~60 s old.
  2. 2On a miss, the tracking API reads track:{awb} from Redis, or the status row plus the last ~30 events from Cassandra, and masks internal details (hub names may be shown, staff names never).
  3. 3If status is OUT_FOR_DELIVERY, the page opens an SSE stream with the rider's live location and 'n stops before you'.

Notify the merchant

  1. 1The notifier consumes StatusChanged events and looks up the merchant's webhook subscriptions.
  2. 2It sends a signed POST with an event_id; failures retry with exponential backoff for up to 24 h, per-merchant queues stop one slow merchant from blocking others.
  3. 3Customer SMS/WhatsApp go only on milestones (shipped, out for delivery, delivered, failed attempt) to control cost.

7 Deep dives

Offline-first scanning: duplicates and out-of-order events

Hubs and delivery vans lose connectivity all the time, so the scanner must never block an operator on the network. Each scan is stored locally with a UUID generated on the device and the device timestamp, and uploaded in batches with retries. This guarantees at-least-once delivery, so the server must be idempotent: dedupe on event_id (Redis set with TTL for speed, and the event's primary key in Cassandra as a natural guard). Events also arrive out of order: a device offline for two hours uploads an 'inbound at Bhiwandi hub' scan after the parcel was already scanned outbound at the next hub. Store every event in the history, sorted by event time, but compute status with rules: a newer status wins over an older one, terminal states (DELIVERED, RTO_DELIVERED) are never regressed by older events, and some transitions are flagged as anomalies (delivered then in-transit) for ops review instead of being applied. Device clocks drift, so record received_at too, and correct obviously wrong device times (in the future, or before pickup) using server time. Kafka partitioning by AWB keeps processing of one shipment serialised.

Status as a projection of the event stream

Treat scan events as the source of truth and the current status as a derived view, which is a light form of event sourcing. The projector is the only writer of shipment_status. For each event, it loads the status row, applies a pure function (current_state, event) → new_state, and writes with a conditional update on version, so a concurrent writer or a retried message cannot apply the same change twice. Because the function is pure, you can replay a shipment's events from Cassandra to rebuild its status after a bug fix, or replay a whole day from S3 into a new projection such as 'hub dwell time'. The projection also holds what tracking needs in one read: current location, next planned hub from the manifest route, attempts count and EDD. Keep the event schema versioned and additive. A common mistake is letting many services update status directly (ops tools, delivery app, returns system); then nobody can explain why a parcel shows 'delivered' in one screen and 'in transit' in another. Ops corrections should also be events (MANUAL_STATUS_OVERRIDE with a reason) so the audit trail stays complete.

Notifications and webhooks at scale

Merchants such as marketplaces and D2C brands rely on webhooks to update their own order pages, so delivery reliability matters more than speed. Build it as an outgoing webhook service: consume StatusChanged events, render a signed payload (HMAC with the merchant's secret) carrying event_id, awb and status, and POST it with a timeout of a few seconds. Keep a separate queue or rate limit per merchant so one merchant whose endpoint is down does not consume all workers; retry with exponential backoff and jitter up to 24 hours, then park in a dead-letter queue with a replay button in the merchant dashboard. Merchants must treat event_id as an idempotency key because retries can duplicate. Order is not guaranteed across retries, so include status_ts and let merchants ignore older updates. Customer messages cost money (SMS, WhatsApp template messages), so send only milestones and collapse rapid changes: if 'out for delivery' and 'delivered' happen within 2 minutes, skip the first. Use regional language templates based on the destination state, which noticeably reduces failed deliveries and support calls.

Expected delivery date (EDD)

The promise shown at checkout and on the tracking page is a prediction, not a lookup. Start from the planned route (origin hub → line-haul → destination hub → last-mile centre) and the historical transit time distribution for each lane and hub, by day of week and season. A model (gradient-boosted trees work well) predicts arrival at each remaining step using features such as current hub backlog, cut-off times for the next truck, holidays, festive load and weather. Recompute on every status change, and also on a timer for shipments that have not moved: a parcel that missed its 22:00 line-haul cut-off is late before any new scan happens. Show a date, not a timestamp, and only move it later once with a reason ('delayed due to heavy rain in Assam'), because flip-flopping erodes trust. Feed predicted delays to ops as exceptions so they can act (re-route, prioritise) before the customer notices. Track calibration: the share of shipments delivered on or before the promised date by lane.

8 Trade-offs

Cassandra for events vs Postgres for everything

Postgres could store ~60M rows/day with partitioning, and is simpler for small companies. Cassandra fits better at this volume: write-heavy, append-only, read by one key (AWB), easy horizontal scaling and TTL. Keep the status projection in a store with conditional writes (DynamoDB or sharded Postgres), and do analytics in a warehouse, not on either.

Push status changes vs merchants polling

Polling the tracking API from thousands of merchants every few minutes multiplies read load and still feels slow. Webhooks push changes once. Offer both: webhooks for real-time and a bulk status API with since-cursor for reconciliation, because webhooks will sometimes be missed.

Strict ordering vs tolerant projection

You cannot guarantee arrival order from offline devices, so do not try to enforce global order. Partition by AWB for per-shipment serial processing, and make the state function tolerant of late events. This is cheaper and more robust than buffering events and waiting for missing ones.

9 Bottlenecks

Festive hub shift-end bursts

Thousands of devices sync at shift end, and a sorter machine can emit hundreds of scans per second. Kafka absorbs bursts; ingestion is stateless and autoscaled; projector consumers scale up to the partition count (plan 128+ partitions). Watch consumer lag as the key SLI: lag directly means stale tracking.

Tracking read spikes

During sales, customers refresh tracking pages repeatedly. CDN caching for 30-60 s and Redis caching per AWB make reads cheap; the origin sees only misses. Collapse concurrent misses for the same AWB (request coalescing).

10 Failure handling

Kafka unavailable

Ingest returns errors and devices keep events in their local queue, retrying later. No scan is lost because the device deletes events only after ack. Tracking shows slightly stale status; alert on device queue depth reported in heartbeats.

Projector bug writes wrong statuses

Because events are the source of truth, fix the code and replay affected AWBs (or a time window) from Cassandra or S3 to rebuild the projection. Suppress notifications during replays so customers do not get duplicate messages.

A merchant's webhook endpoint is down for hours

Its per-merchant queue backs off and grows while others are unaffected. After 24 h events move to a dead-letter queue; the merchant can replay them or use the bulk status API to reconcile.

11 What impresses

  • Starts from offline devices and designs for duplicates and out-of-order arrival from the beginning.
  • Separates the event history from the current-status projection and explains replay after bugs.
  • Never lets an older event regress a terminal state, and flags impossible transitions for ops.
  • Uses per-merchant queues for webhooks so one bad endpoint cannot block others.
  • Recomputes EDD on missed cut-offs even without a new scan.
  • Treats consumer lag as the key freshness SLI.

12 Mistakes

  • A single status column updated directly by many services.
  • Assuming events arrive in order and once.
  • Using server receive time as event time without keeping device time.
  • Sending an SMS on every scan event.
  • Querying Cassandra with filters other than the partition key for ops searches.
  • No plan for a device that is offline for hours.

13 Follow-ups

Q1How do you track a bag of 50 parcels with one scan?
Q2How would you detect a parcel that is stuck at a hub?
Q3A device's clock is three hours wrong. What happens and how do you fix it?
Q4How do you handle returns (RTO) and their timeline?
Q5How would you expose tracking for a marketplace that ships with five carriers?
Q6How do you keep 2 years of history cheaply and still answer disputes?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free