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:
- Querying nearest available truck in Postgres with lat/lng requires scanning all trucks with distance calculation β O(N) full table scan
- Single API server cannot handle 300K+ GPS pings per second from active fleet
- No way to push live location updates to shippers β polling is wasteful and adds latency
- If two shippers request similar routes simultaneously and the same truck is best for both β double-assignment
- ETA is static β computed once at booking, never updated as truck encounters traffic or delays
- No route-awareness β system might assign a truck 200km away when another truck is on the same highway heading toward the pickup
The rest of the doc evolves this into a production-grade allocation and tracking system.
3. Prior Art Weβre Drawing From
- Uber Freight Marketplace β Real-time load matching platform connecting shippers with carriers. Uses scoring algorithms that weight price, carrier reliability, proximity, and route alignment. (Uber Freight Engineering)
- Convoy Dead-Head Optimization β Minimizes empty miles by matching trucks to loads near their current route terminus. Uses graph-based route planning to batch multiple shipments on a single truck. (Convoy Engineering)
- Uber H3 Hexagonal Grid β Used for geospatial partitioning of supply and demand. Each hexagon represents a region for allocation scoring and demand prediction. (Uber H3)
- Google Maps ETA Engine β Real-time traffic-aware routing using live GPS probe data. Historical patterns + live anomalies predict segment-level travel times. Google published this as part of their DeepMind collaboration on traffic prediction.
- Rivigo Relay Model β Indian freight company that uses relay points every 300km so drivers can be swapped. System must track truck AND driver separately, recompute ETA at each relay. Shows the importance of event-driven state machines for shipment lifecycle.
4. Functional Requirements
Core (Top 3)
- Shipper books a truck β specify source, destination, weight, goods type, pickup window. System allocates the best available truck.
- Real-time tracking β shipper sees live truck position on a map throughout transit with updated ETA.
- Smart allocation β system assigns trucks maximizing utilization (minimize deadhead miles) considering proximity, route alignment, capacity, and carrier rating.
Below the Line
- Carrier can accept/decline assigned loads
- Multi-stop routes (truck picks up multiple shipments along corridor)
- Dynamic pricing based on demand and route popularity
- Shipment status notifications (picked up, in transit, delayed, delivered)
- Proof of delivery (photo/signature)
- Invoice and payment settlement
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
- ETA accuracy within 10 minutes of actual arrival
- Handle 10K bookings/day
- Sub-second WebSocket push for live tracking
- Idempotent booking API (retry-safe)
6. Scale Estimation (Back-of-Envelope)
- Fleet: 50K active trucks at peak, 200K registered
- Write QPS: 300K location pings/sec (GPS devices)
- Booking QPS: ~0.1/sec (10K/day β not the bottleneck)
- Read QPS: 5K tracking queries/sec (shippers checking shipments)
- Storage: Tracking history ~500GB/year (50K trucks x 6 pings/min x 365 days x 50 bytes)
7. Core Entities
- Shipper β company that needs goods moved. Has account, payment method, shipment history.
- Carrier β trucking company that owns trucks. Has fleet, rating, compliance docs.
- Truck β physical vehicle. Has capacity, type, current location, status, assigned carrier.
- Shipment β booking unit. Source, destination, weight, status (state machine), assigned truck.
- Route β predefined corridor between regions with waypoints, distance, typical duration.
- LocationPing β ephemeral GPS data. truckId, lat/lng, speed, heading, timestamp.
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:
- Booking Service β validates request, creates shipment record, triggers allocation
- Allocation Service β queries nearby trucks, scores candidates, claims winner
- Redis Geo β stores real-time truck positions for proximity queries
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):
- Shipper calls
POST /shipmentswith pickup/drop locations and weight - API Gateway authenticates and rate-limits, forwards to Booking Service
- Booking Service validates inputs, inserts shipment with status=PENDING into Postgres
- Booking Service calls Allocation Service with shipment details
- Allocation Service runs
GEOSEARCH trucks:available:{region} FROMLONLAT pickupLng pickupLat BYRADIUS 100km COUNT 20 ASC - For each candidate truck: fetch
truck:meta:{truckId}from Redis, filter by capacity >= weight and matching truck type - Score remaining candidates:
score = 0.5 * (1/distance) + 0.3 * route_alignment + 0.2 * carrier_rating - Attempt atomic assignment on top candidate:
UPDATE trucks SET status='ASSIGNED', current_shipment_id=? WHERE truck_id=? AND status='AVAILABLE' - If
rowsAffected=0(truck was taken), try next candidate - 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:
- Location Ingestion Service β consumes GPS pings from Kafka, writes to Redis
- Kafka β durable buffer for 300K pings/sec, enables replay on failure
- WebSocket Service β maintains long-lived connections with shippers, pushes updates
- Redis Pub/Sub β channels per shipment for fan-out from ingestion to WebSocket
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:
- Truck GPS device sends position every 10 seconds to
POST /trucks/{id}/location - API Gateway publishes to Kafka topic
truck-location-pings(key = truckId, for partition affinity) - Location Ingestion Service consumes from Kafka
- Updates Redis Geo:
GEOADD trucks:available:{region} lng lat truckId - Updates Redis Cache:
HSET truck:meta:{truckId} lat {lat} lng {lng} speed {speed} lastPing {ts} - Looks up if this truck has an active shipment. If yes: publishes to
PUBLISH track:{shipmentId} {lat, lng, speed, eta} - WebSocket Service (subscribed to that channel) pushes the update to the connected shipper
- Shipper sees truck dot move on the map
FR3: ETA Computation
ETA must update as the truck progresses and encounters delays.
New components:
- ETA Service β periodic job that recomputes arrival times for all active shipments
- Maps API β external service for traffic-aware route duration
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:
- ETA Cron runs every 2 minutes
- Fetches all shipments with status IN_TRANSIT from Postgres
- For each: gets truckβs current position from Redis
- Calls Maps API: current position -> destination with traffic awareness
- Stores updated ETA in Redis:
SET eta:{shipmentId} {etaTimestamp} EX 180 - 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.
- At 300K writes/sec, Postgres row-level locking on the
truckstable creates massive contention. WAL becomes the bottleneck. Response times degrade to seconds.
Good: Write to Redis directly from the API.
- Redis handles 100K+ ops/sec per instance. With 3-4 Redis shards (by region), 300K is easy. But if Redis crashes between a ping and consumption, that ping is lost. No replay capability.
Great: Kafka as ingestion buffer + Redis as hot store.
- GPS device -> Kafka (durable, append-only, handles millions/sec). Consumer reads from Kafka, writes to Redis Geo + Redis Hash.
- If Redis crashes: replay from Kafka offset. Zero data loss.
- If consumer crashes: new consumer picks up from last committed offset.
- Kafka partitioned by truckId β ordering preserved per truck (important: we always want the latest position to win).
- Redis stores only the latest position per truck (GEOADD overwrites). Memory footprint stays constant at ~50K entries regardless of time.
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.
- Race condition: two Allocation Service instances both see the truck as available, both try to assign. One succeeds, other overwrites β or worse, both succeed with different shipments.
Good: Use UPDATE ... WHERE status=AVAILABLE as an atomic CAS (compare-and-swap).
UPDATE trucks SET status='ASSIGNED', current_shipment_id=? WHERE truck_id=? AND status='AVAILABLE'. CheckrowsAffected. If 0 β truck was taken. Try next candidate.- Works perfectly at 10K bookings/day (~0.1 TPS). Row-level lock contention is negligible.
Great: Same as Good + retry with pre-sorted candidates.
- Allocation Service fetches top 10 candidates sorted by score. Tries assignment in order. First success wins. Expected 1-2 attempts max. Latency: 50-100ms.
- No distributed lock needed. Postgres row-level locking is sufficient at this booking rate.
- Bonus: add
versioncolumn for optimistic locking if we ever need multi-statement transactions.
Deep Dive 3: ETA Accuracy
Bad: Compute ETA once at booking time using straight-line distance / average speed.
- Ignores traffic, road network, stops, detours. Off by hours on long routes.
Good: Call Maps API at booking + every 30 minutes.
- Traffic-aware routing gives much better accuracy. But 30-min interval means shipper doesnβt see delays until well after they happen.
Great: Recompute every 2 minutes for active shipments + anomaly detection.
- ETA Service pulls all IN_TRANSIT shipments, fetches current truck position from Redis, calls Maps API for remaining route duration.
- Store computed ETA in Redis with 3-min TTL.
- If new ETA deviates by > 15 minutes from previous: trigger a proactive notification to the shipper (βYour shipment is delayed by ~20 min due to traffic on NH-48β).
- Cost control: batch API calls. Maps API supports batch routing β send 50 origin-destination pairs in one request.
- Fallback: if Maps API is down, compute
remaining_distance / average_speed_from_last_10_pings. Less accurate but never fails.
Deep Dive 4: WebSocket Scaling for Live Tracking
Bad: Each shipper polls GET /track every 5 seconds.
- 5K active shippers x 1 req/5s = 1K req/sec. Most responses are identical to the previous (truck hasnβt moved much in 5s). Wasteful.
Good: WebSocket connection per shipper. Push on every GPS ping.
- Efficient β only sends data when position changes. But 50K trucks x 6 pings/min = 300K pushes. If 1000 shippers are each watching one truck: 300K events fan-out to WebSocket connections.
Great: WebSocket + debounce + Redis Pub/Sub for routing.
- Location Ingestion publishes to Redis Pub/Sub channel
track:{shipmentId}only if position changed by > 50 meters since last push (debounce). - WebSocket Service instances subscribe to channels for their connected shippers.
- Reduces push volume by ~60% (most 10s intervals on highway = same trajectory, only publish when meaningful change).
- Scaling: WebSocket instances are stateful. Use consistent hashing by shipmentId to route shippers to the same instance. If instance dies, clients reconnect to another.
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