One Gateway for DHL, DPD, and GLS Tracking
The Situation
Parcel tracking breaks down when operations teams have to check carrier portals one shipment at a time. DHL, DPD, and GLS each expose tracking data differently. One needs an API key. One can return HTML on error. One sends local timestamp fields that need normalization before they can be compared.
The product requirement was a backend gateway, not a dashboard. Client systems register shipments through REST, read the current state when needed, and subscribe over WebSocket for live changes. The gateway owns the messy part: polling carriers, normalizing statuses, removing duplicates, preserving the event history, and pushing updates to connected clients.
The target was concrete: 3 carriers, 100 active shipments, 500 events per minute, and WebSocket delivery under 200ms after a status change enters the event pipeline. The build shipped locally and in production-like Docker Compose, with live deployment intentionally removed from scope.
The Cost of Doing Nothing
The old operating shape is visible in the PRD: teams checking DHL, DPD, and GLS portals manually instead of reading one source. At 100 active shipments, even a conservative one-minute status check per shipment turns into more than an hour and a half per sweep. At a $25/hour support cost, one daily sweep burns roughly $8K a year before counting missed updates, customer escalations, or repeated checks during delivery windows.
The larger cost is trust. If a dashboard updates late because a carrier status was missed, the customer does not blame the carrier portal. They blame the business that gave them stale tracking. A tracking gateway has to make freshness and auditability part of the infrastructure, not part of a support agent's routine.
What I Built
I built a TypeScript and Fastify backend that turns three carrier feeds into one event stream. Shipments live in PostgreSQL. Every carrier status change becomes an immutable row in tracking_events. The visible shipment status is only a projection, updated when the new carrier timestamp is newer than the current one.
Redis Streams handles live delivery. The event processor publishes to tracking:events after PostgreSQL accepts the event. A WebSocket broadcaster consumes that stream as group ws-broadcaster, finds subscribed connections by tracking number, and sends JSON payloads to clients. If Redis fails after the database write, the event is still stored. If the database write fails, nothing is published.
The difficult part was not the WebSocket route. It was deciding which failure was acceptable. Missing a live push is recoverable because the REST timeline can be read again. Losing or inventing a carrier event is not. That ordering drove the processor, the Redis contract, and the integration tests.
System Flow
Data Model
Architecture Layers
The Decision Log
| Decision | Alternative Rejected | Why |
|---|---|---|
| Store tracking events before publishing to Redis | Publish live updates first | The event timeline is the record. Live delivery is allowed to fail after persistence, not before it. |
Use deterministic dedup_key values |
Treat each poll result as new | Carrier retries and duplicate scans must collapse into one row. |
| Keep carrier adapters separate | Build one shared carrier parser | DHL, DPD, and GLS differ in auth, error payloads, and timestamp formats. |
| Use Redis Streams for fanout | Send directly from the processor | Consumer groups give acknowledgement and stuck-message recovery. |
| Soft-delete shipments | Delete rows | Polling should stop while the event history remains queryable. |
| API keys only | User accounts and JWT | The gateway is consumed by trusted systems, not end users. |
Results
The local build now covers the full gateway path: register shipments, poll or mock carrier events, persist the event, suppress duplicates, update current status, publish to Redis, and deliver to WebSocket subscribers. The latest build journal records 134 passing Vitest tests, 83.32% statement and line coverage for unit and integration suites, a production-start check, a Docker Compose production smoke test, and a mock-carrier journey covering DHL, DPD, and GLS.
Before the gateway, the operating model was one carrier portal per shipment check. After the gateway, client systems get one API, one event schema, and one live stream. The system is not live-hosted, and the 24-hour simulated-load soak remains deferred. That is the honest boundary. The architecture is ready for local and self-hosted use; long-running production proof still needs wall-clock runtime under 100 active shipments and 3 carriers.
PostgreSQL has headroom at this scale; sequential per-shipment carrier polling is the next pressure point. If shipment volume grows past the current target, the poller needs controlled parallelism and persisted carrier error counters before the dashboard numbers can be trusted.