Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 20 min read

Designing a Freight Logistics Platform (Uber Freight)

Difficulty: Advanced Topics: Geo Matching, Real-Time Tracking, Route Optimization, Allocation, WebSocket Asked at: Uber Freight, Rivigo, BlackBuck, Delhivery, Amazon Logistics Prerequisites: Geospatial Indexing, WebSockets, Message Queues, and Caching


1. Understanding the Problem

A freight logistics platform connects shippers (companies with cargo) to carriers (truck owners/drivers). A shipper posts a shipment request with source, destination, weight, and pickup window. The system finds the best available truck considering proximity, route alignment, and capacity, assigns it, and then provides real-time tracking with accurate ETA to the shipper throughout transit. Unlike ride-sharing (seconds-scale matching), freight operates on minutes-to-hours timescales, but the tracking and ETA requirements are equally real-time.


2. Naive First Cut

flowchart LR
    Shipper["Shipper App"]:::client
    API["API Server"]:::service
    DB["Postgres DB"]:::data
    Truck["Truck GPS"]:::client

    Shipper --> API
    Truck --> API
    API --> DB

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
Color Meaning
Purple Client / Device
Green Backend services
Yellow Data stores

How this breaks:

The rest of the doc evolves this into a production-grade allocation and tracking system.


3. Prior Art We’re Drawing From


4. Functional Requirements

Core (Top 3)

  1. Shipper books a truck – specify source, destination, weight, goods type, pickup window. System allocates the best available truck.
  2. Real-time tracking – shipper sees live truck position on a map throughout transit with updated ETA.
  3. Smart allocation – system assigns trucks maximizing utilization (minimize deadhead miles) considering proximity, route alignment, capacity, and carrier rating.

Below the Line


5. Non-Functional Requirements

Core

NFR Target
Location ingestion 50K active trucks x 6 pings/min = 300K writes/sec
Allocation latency Truck assigned within 60 seconds of booking request
Tracking freshness Shipper sees location no older than 15 seconds
Booking consistency Strong – no double-assignment of same truck to two shipments
Availability 99.9% – shipper can always book and track

Below the Line

6. Scale Estimation (Back-of-Envelope)


7. Core Entities


8. API / System Interface

POST /api/v1/shipments
  Body: { shipperId, pickupLat, pickupLng, dropLat, dropLng, weightTons, goodsType, pickupWindowStart, pickupWindowEnd }
  Response: { shipmentId, assignedTruckId, estimatedPickupTime, eta, status: "ASSIGNED" }
  Auth: JWT Bearer (shipper)
  Note: Idempotent via X-Idempotency-Key header. Never trust client for shipperId -- derive from token.

GET /api/v1/shipments/{shipmentId}/track
  Response: { truckLat, truckLng, speed, heading, eta, lastUpdated, status }
  Auth: JWT Bearer (shipper -- must own this shipment)

WebSocket /ws/v1/shipments/{shipmentId}/live
  Pushes: { truckLat, truckLng, speed, eta, updatedAt } every 10s
  Auth: JWT ticket in connection params

POST /api/v1/trucks/{truckId}/location (called by GPS device)
  Body: { lat, lng, speed, heading, timestamp }
  Response: 204 No Content
  Auth: Device API key
  Note: Fire-and-forget. High volume -- 6 calls/min per truck.

GET /api/v1/shipments/{shipmentId}
  Response: { full shipment details, truck info, timeline of events }
  Auth: JWT Bearer (shipper)

9. High-Level Design

FR1: Shipper Books a Truck

The shipper submits a booking. The Allocation Service finds the best truck and assigns it atomically.

Components introduced:

flowchart LR
    S["Shipper App"]:::client
    GW["API Gateway"]:::edge
    BS["Booking Service"]:::service
    AS["Allocation Service"]:::service
    RG["Redis Geo"]:::data
    PG["Postgres"]:::data

    S --> GW
    GW --> BS
    BS --> AS
    AS --> RG
    AS --> PG
    BS --> PG

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef edge fill:#1a3652,stroke:#60a5fa,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

Flow (numbered steps):

  1. Shipper calls POST /shipments with pickup/drop locations and weight
  2. API Gateway authenticates and rate-limits, forwards to Booking Service
  3. Booking Service validates inputs, inserts shipment with status=PENDING into Postgres
  4. Booking Service calls Allocation Service with shipment details
  5. Allocation Service runs GEOSEARCH trucks:available:{region} FROMLONLAT pickupLng pickupLat BYRADIUS 100km COUNT 20 ASC
  6. For each candidate truck: fetch truck:meta:{truckId} from Redis, filter by capacity >= weight and matching truck type
  7. Score remaining candidates: score = 0.5 * (1/distance) + 0.3 * route_alignment + 0.2 * carrier_rating
  8. Attempt atomic assignment on top candidate: UPDATE trucks SET status='ASSIGNED', current_shipment_id=? WHERE truck_id=? AND status='AVAILABLE'
  9. If rowsAffected=0 (truck was taken), try next candidate
  10. On success: update shipment status to ASSIGNED, return confirmation with ETA

FR2: Real-Time Location Tracking

Trucks emit GPS pings. The system ingests at scale, updates position stores, and pushes to watching shippers.

New components:

flowchart LR
    T["Truck GPS Device"]:::client
    KF["Kafka: truck-locations"]:::async
    LI["Location Ingestion"]:::service
    RG["Redis Geo"]:::data
    RC["Redis Cache"]:::data
    PS["Redis Pub/Sub"]:::async
    WSS["WebSocket Service"]:::service
    S["Shipper App"]:::client

    T --> KF
    KF --> LI
    LI --> RG
    LI --> RC
    LI --> PS
    PS --> WSS
    WSS --> S

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef async fill:#3b2a4c,stroke:#a78bfa,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

Flow:

  1. Truck GPS device sends position every 10 seconds to POST /trucks/{id}/location
  2. API Gateway publishes to Kafka topic truck-location-pings (key = truckId, for partition affinity)
  3. Location Ingestion Service consumes from Kafka
  4. Updates Redis Geo: GEOADD trucks:available:{region} lng lat truckId
  5. Updates Redis Cache: HSET truck:meta:{truckId} lat {lat} lng {lng} speed {speed} lastPing {ts}
  6. Looks up if this truck has an active shipment. If yes: publishes to PUBLISH track:{shipmentId} {lat, lng, speed, eta}
  7. WebSocket Service (subscribed to that channel) pushes the update to the connected shipper
  8. Shipper sees truck dot move on the map

FR3: ETA Computation

ETA must update as the truck progresses and encounters delays.

New components:

flowchart LR
    CRON["ETA Cron - every 2 min"]:::service
    PG["Postgres: active shipments"]:::data
    RC["Redis: truck positions"]:::data
    MAPS["Maps API"]:::external
    STORE["Redis: eta cache"]:::data
    PS["Pub/Sub: notify shipper"]:::async

    CRON --> PG
    CRON --> RC
    CRON --> MAPS
    CRON --> STORE
    CRON --> PS

    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
    classDef async fill:#3b2a4c,stroke:#a78bfa,color:#e2e8f0
    classDef external fill:#4c2a3a,stroke:#fb7185,color:#e2e8f0

Flow:

  1. ETA Cron runs every 2 minutes
  2. Fetches all shipments with status IN_TRANSIT from Postgres
  3. For each: gets truck’s current position from Redis
  4. Calls Maps API: current position -> destination with traffic awareness
  5. Stores updated ETA in Redis: SET eta:{shipmentId} {etaTimestamp} EX 180
  6. If ETA changed significantly (> 10 min drift): publish update via Pub/Sub to notify shipper

10. Technology Choices

Tier Purpose Stores Access Pattern Primary Alternatives
Location Store (hot) Real-time truck positions lat/lng per active truck Geo-radius queries + point updates Redis Geo PostGIS, Elasticsearch geo_point
Booking DB Shipments, trucks, assignments Relational data with joins CRUD by shipmentId, truckId Postgres CockroachDB, MySQL
Event Bus Location events and state transitions GPS pings, status changes Pub/sub + ordered per truck Kafka Kinesis, Redpanda
Cache ETA, truck availability, route data Precomputed values High-QPS reads with TTL Redis Memcached
Real-time Delivery Push truck position to shippers WebSocket messages Fan-out per active shipment WebSocket Gateway + Redis Pub/Sub SSE, gRPC streaming
History Store Historical truck paths Time-series GPS data Batch writes, range queries by time Cassandra or TimescaleDB InfluxDB, ClickHouse
Maps/Routing ETA and route computation Road network + traffic Request-response per computation External Maps API (Google/Mapbox) Self-hosted OSRM

Why Redis Geo over Postgres + PostGIS for truck locations? 300K writes/sec is pure in-memory territory. PostGIS would require disk I/O on every update. Redis Geo uses an in-memory sorted set with geohash encoding – sub-millisecond for both writes and proximity queries. We only need the latest position per truck (not history), so Redis’s memory footprint stays bounded at ~50K entries.

Why Kafka for GPS pings and not direct Redis writes? If the Location Ingestion service crashes between receiving a ping and writing to Redis, the ping is lost forever. Kafka gives us durability and replay. Consumers can rebuild Redis from Kafka on crash recovery. Also decouples ingestion (300K/sec bursty) from processing (variable consumer speed).

Why WebSocket over polling for shipper tracking? A shipper tracking an active shipment wants updates every 10 seconds. With polling: 5K shippers x 1 req/10s = 500 req/sec – wasteful since most responses are β€œsame position.” WebSocket: single connection per active tracker, push only when position changes. Saves bandwidth and reduces latency from 10s (polling interval) to ~100ms (push on update).


11. Data Modeling

Redis Geo (Truck Positions – real-time):

Key: "trucks:available:{region}" -> Geo Set
  Member: truckId
  Score: geohash-encoded lat/lng

Key: "truck:meta:{truckId}" -> Hash
  { status, capacity, type, heading, speed, lastPing, currentShipmentId }
  TTL: 60s (auto-expires if truck stops pinging -- marks as offline)

Key: "eta:{shipmentId}" -> String
  Value: "2026-08-19T14:30:00Z"
  TTL: 120s (recomputed every 2 min)

Postgres (Booking DB):

CREATE TABLE trucks (
    truck_id UUID PRIMARY KEY,
    carrier_id UUID NOT NULL,
    capacity_tons DECIMAL(6,2),
    truck_type VARCHAR(30),  -- FLATBED, REFRIGERATED, CONTAINER, OPEN
    status VARCHAR(20) NOT NULL,  -- AVAILABLE, ASSIGNED, IN_TRANSIT, MAINTENANCE
    current_lat DECIMAL(10,7),
    current_lng DECIMAL(10,7),
    last_location_at TIMESTAMP
);
CREATE INDEX idx_trucks_status ON trucks(status);
CREATE INDEX idx_trucks_carrier ON trucks(carrier_id);

CREATE TABLE shipments (
    shipment_id UUID PRIMARY KEY,
    shipper_id UUID NOT NULL,
    truck_id UUID,
    status VARCHAR(20) NOT NULL,  -- PENDING, ASSIGNED, PICKED_UP, IN_TRANSIT, DELIVERED, CANCELLED
    pickup_lat DECIMAL(10,7),
    pickup_lng DECIMAL(10,7),
    drop_lat DECIMAL(10,7),
    drop_lng DECIMAL(10,7),
    weight_tons DECIMAL(6,2),
    goods_type VARCHAR(50),
    pickup_window_start TIMESTAMP,
    pickup_window_end TIMESTAMP,
    estimated_delivery_at TIMESTAMP,
    actual_delivery_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT NOW(),
    assigned_at TIMESTAMP,
    idempotency_key UUID UNIQUE
);
CREATE INDEX idx_shipments_shipper ON shipments(shipper_id, created_at DESC);
CREATE INDEX idx_shipments_truck ON shipments(truck_id, status);
CREATE INDEX idx_shipments_status ON shipments(status);

CREATE TABLE routes (
    route_id UUID PRIMARY KEY,
    source_region VARCHAR(50),
    dest_region VARCHAR(50),
    waypoints JSONB,  -- [{lat, lng, name}]
    distance_km INT,
    typical_duration_hours DECIMAL(5,2)
);

Kafka Topics:

Topic: truck-location-pings (partitioned by truckId)
  Key: truckId
  Value: { truckId, lat, lng, speed, heading, timestamp }
  Retention: 24 hours

Topic: shipment-events (partitioned by shipmentId)
  Key: shipmentId
  Value: { shipmentId, eventType, truckId, timestamp, metadata }
  Events: CREATED, ASSIGNED, PICKED_UP, IN_TRANSIT, DELAYED, DELIVERED, CANCELLED
  Retention: 7 days

Access Patterns:

Query Data Source How
Find nearest available trucks Redis Geo GEOSEARCH trucks:available:{region} FROMLONLAT lng lat BYRADIUS 100 km COUNT 20 ASC
Update truck position Redis Geo GEOADD trucks:available:{region} lng lat truckId (from Kafka consumer)
Assign truck to shipment Postgres UPDATE trucks SET status='ASSIGNED' WHERE truck_id=? AND status='AVAILABLE' (row lock)
Get current ETA Redis GET eta:{shipmentId}
Live tracking push Redis Pub/Sub Publish to channel track:{shipmentId} on each location update
Shipment history Postgres SELECT * FROM shipments WHERE shipper_id=? ORDER BY created_at DESC
Truck path replay Cassandra Range query by truckId + time window

12. Deep Dives

Deep Dive 1: Handling 300K Location Writes/sec

Bad: Write every GPS ping directly to Postgres.

Good: Write to Redis directly from the API.

Great: Kafka as ingestion buffer + Redis as hot store.

flowchart LR
    GPS["50K Trucks"]:::client
    KF["Kafka 6 partitions"]:::async
    C1["Consumer 1"]:::service
    C2["Consumer 2"]:::service
    C3["Consumer 3"]:::service
    R1["Redis Shard 1 - North"]:::data
    R2["Redis Shard 2 - South"]:::data
    R3["Redis Shard 3 - West"]:::data

    GPS --> KF
    KF --> C1
    KF --> C2
    KF --> C3
    C1 --> R1
    C2 --> R2
    C3 --> R3

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef async fill:#3b2a4c,stroke:#a78bfa,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

Deep Dive 2: Allocation – Preventing Double-Assignment

Bad: Check status=AVAILABLE then update in two separate queries.

Good: Use UPDATE ... WHERE status=AVAILABLE as an atomic CAS (compare-and-swap).

Great: Same as Good + retry with pre-sorted candidates.

Deep Dive 3: ETA Accuracy

Bad: Compute ETA once at booking time using straight-line distance / average speed.

Good: Call Maps API at booking + every 30 minutes.

Great: Recompute every 2 minutes for active shipments + anomaly detection.

Deep Dive 4: WebSocket Scaling for Live Tracking

Bad: Each shipper polls GET /track every 5 seconds.

Good: WebSocket connection per shipper. Push on every GPS ping.

Great: WebSocket + debounce + Redis Pub/Sub for routing.


13. Design Self-Audit

Potential weakness Assessment
Dedicated search index? Not needed – we’re not doing text search. Geo queries handled by Redis Geo.
Stale reads after writes? Allocation reads from Postgres (source of truth, not cache). Tracking reads from Redis with 10s staleness – acceptable.
Single point of failure? Redis: use Redis Cluster with replicas. Kafka: 3 brokers with replication factor 2. Postgres: primary + sync replica.
Dead-letter for async failures? Kafka consumer failures: retry 3x, then send to DLQ topic. Alert ops for manual review.
Data freshness across CDC/cache? Truck status in Postgres is the source of truth. Redis Geo is updated from Kafka (eventual, <1s lag). Allocation reads Postgres for assignment (strong).
Cost at hot tiers? Redis: 50K entries x 100 bytes = 5MB. Trivial. Kafka: 300K msg/sec x 100 bytes x 24h retention = ~2.5TB/day. Moderate. Use tiered storage.

14. Core Flows

Booking Flow (end-to-end)

sequenceDiagram
    participant S as Shipper
    participant GW as API Gateway
    participant BS as Booking Service
    participant AS as Allocation Service
    participant RG as Redis Geo
    participant PG as Postgres
    participant NS as Notification Service

    S->>GW: POST /shipments
    GW->>BS: Forward (authenticated)
    BS->>PG: INSERT shipment (status=PENDING)
    BS->>AS: Allocate truck for shipment
    AS->>RG: GEOSEARCH near pickup 100km
    RG-->>AS: [truck1: 8km, truck2: 25km, truck3: 60km]
    AS->>AS: Score by distance + route + rating
    AS->>PG: UPDATE trucks SET status=ASSIGNED WHERE id=truck1 AND status=AVAILABLE
    alt Row locked - truck taken
        AS->>PG: Try truck2
    end
    PG-->>AS: Success (rowsAffected=1)
    AS-->>BS: Allocated truck1
    BS->>PG: UPDATE shipment SET status=ASSIGNED, truck_id=truck1
    BS->>NS: Notify carrier (push + SMS)
    BS-->>S: { shipmentId, truckId, eta }

Tracking Flow (real-time)

sequenceDiagram
    participant T as Truck GPS
    participant KF as Kafka
    participant LI as Location Ingestion
    participant RD as Redis
    participant PS as Pub/Sub
    participant WSS as WebSocket Service
    participant S as Shipper

    T->>KF: {truckId, lat, lng, speed} every 10s
    KF->>LI: Consume
    LI->>RD: GEOADD + HSET truck position
    LI->>LI: Lookup active shipment for truck
    LI->>PS: PUBLISH track:shipment123 {lat, lng, eta}
    PS->>WSS: Receive on subscribed channel
    WSS->>S: WebSocket push to shipper
    Note over S: Map updates with new truck position

Shipment State Machine

PENDING --> ASSIGNED --> PICKED_UP --> IN_TRANSIT --> DELIVERED
    |           |                          |
    v           v                          v
 CANCELLED   CANCELLED                 DELAYED --> IN_TRANSIT

15. Final Architecture

flowchart TB
    SA["Shipper App"]:::client
    TD["Truck GPS Device"]:::client
    GW["API Gateway"]:::edge
    BS["Booking Service"]:::service
    AS["Allocation Service"]:::service
    TS["WebSocket Service"]:::service
    LI["Location Ingestion"]:::service
    ETA["ETA Service"]:::service
    NS["Notification Service"]:::service
    KF["Kafka"]:::async
    PS["Redis Pub/Sub"]:::async
    PG["Postgres"]:::data
    RG["Redis Geo + Cache"]:::data
    CS["Cassandra: history"]:::data
    MAPS["Maps API"]:::external

    SA --> GW
    GW --> BS
    GW --> TS
    BS --> AS
    AS --> RG
    AS --> PG
    BS --> PG
    BS --> NS

    TD --> GW
    GW --> KF
    KF --> LI
    LI --> RG
    LI --> PS
    LI --> CS
    PS --> TS
    TS --> SA

    ETA --> RG
    ETA --> MAPS
    ETA --> PS

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef edge fill:#1a3652,stroke:#60a5fa,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef async fill:#3b2a4c,stroke:#a78bfa,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
    classDef external fill:#4c2a3a,stroke:#fb7185,color:#e2e8f0
Color Component Type
Purple Clients / Devices
Blue Edge (API Gateway)
Green Backend Services
Dark Purple Async (Kafka, Pub/Sub)
Yellow Data Stores
Pink External Services

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access