Designing a Real-Time Shipment Tracking System at Scale
A complete, from-first-principles walkthrough of how large e-commerce and logistics platforms aggregate shipment status from many third-party carriers — FedEx, UPS, DHL, USPS, and regional couriers — and present customers with one unified, near-real-time tracking timeline.
Introduction & History
Order a product online today and within seconds you get a tracking number. Click it, and a timeline appears: label created, picked up, in transit, arrived at a regional hub, out for delivery, delivered. That timeline feels like a single, simple thing.
Underneath it, however, is often a package that physically passed through two or three completely different shipping companies, each with its own tracking system, its own data format, its own update frequency, and its own definition of what “in transit” even means. A shipment tracking aggregation system is the software layer that takes all of that fragmented, inconsistent carrier data and turns it into one clean, trustworthy timeline for the customer.
A short history of shipment tracking
Barcode scanning at handling points
Couriers have used barcode scanning at each handling point since the 1980s, when companies like UPS began equipping drivers with handheld scanners (DIAD devices) to record pickup and delivery events electronically instead of on paper manifests.
Carrier websites and check-a-number pages
Each carrier built its own tracking website; customers looked up their package on whichever carrier’s site matched their tracking number, one carrier at a time.
Retailer-owned unified tracking pages
Large e-commerce retailers began aggregating carrier data behind their own tracking pages so customers never had to leave the retailer’s site, kicking off the multi-carrier normalization problem in earnest.
Webhook-driven, event-first architectures
Carriers added webhook support alongside polling APIs, letting retailers move from “check every N minutes” polling to push-based, near-real-time updates for the carriers that supported it.
Multi-carrier aggregators and predictive ETAs
A category of dedicated shipment-tracking platforms emerged offering hundreds of carrier integrations behind a single API, and modern retailers now feed real scan history into learned models to refine estimated delivery dates continuously.
Package tracking itself is not new. What has changed dramatically since then is scale and expectation. A single large e-commerce retailer today might ship through five, ten, or more carrier partners simultaneously — some international, some regional, some last-mile specialists — and customers expect one unified “where is my order” experience regardless of which carrier is actually holding the box at any given moment.
This shift from “check the carrier’s own website” to “check the retailer’s own unified tracking page” is what turned shipment tracking from a simple API integration into a genuine distributed systems problem. The system has to pull or receive data from many external, unreliable, inconsistent sources; normalize wildly different event vocabularies into one shared model; deduplicate and reorder events that can arrive out of sequence; and push updates to customers fast enough that the experience feels live, not stale.
Imagine a school trip where different groups of students travel by different buses, each bus driver calling in a status update to a different phone number, in a different format, at different intervals. A shipment tracking system is the single dispatcher’s desk that takes every one of those inconsistent phone calls and turns them into one shared whiteboard that any parent can glance at and instantly understand, regardless of which bus their child is actually on.
This guide focuses on that generation of systems: multi-carrier, event-driven, and built on the same distributed systems building blocks used across modern backend engineering — API gateways, load balancers, message queues, caches, and webhook-driven integrations — applied specifically to the problem of unifying shipment visibility across many independent, external data sources.
The Problem & Why a Naive Approach Fails
It is worth being precise about why this is hard, because the naive version of this system — “just call the carrier’s API whenever the customer opens the tracking page” — breaks down quickly at real scale, for several concrete reasons.
1. Every carrier speaks a different language
One carrier’s API might report a status called "IT" for in-transit; another might use "InTransit"; a third might use a numeric status code that has to be looked up in a separate table. Some carriers report granular scan events at every hub; others only report a handful of milestone events. Without a normalization layer, a retailer’s tracking page would need carrier-specific logic scattered everywhere it displays a status, which becomes unmaintainable as more carriers are added.
2. Live calls don’t scale or stay reliable
If every customer page view triggered a live API call out to the relevant carrier, the system’s reliability and latency would be entirely at the mercy of external services the retailer does not control. Carrier APIs have their own rate limits, occasional outages, and inconsistent latency; a naive design ties the customer’s experience directly to all of those external failure modes at the worst possible moment — when the customer is actively checking on their order.
3. Push data beats constant polling
Polling every tracking number on a fixed schedule, regardless of whether anything has actually changed, wastes enormous amounts of API quota and compute, especially for a retailer with millions of active shipments at any given time. Many carriers support webhooks that push an update the moment a scan event occurs, which is far more efficient, but not every carrier supports this, and webhook delivery itself is not always reliable, so a pure push-only design cannot be trusted in isolation either.
4. Out-of-order and duplicate events are the norm
Distributed carrier networks routinely deliver events out of order — a “delivered” webhook can occasionally arrive before an “out for delivery” event finishes processing, and network retries can cause the same scan event to be delivered more than once. A system that naively overwrites status on every incoming update, without any ordering or deduplication logic, will intermittently show customers a timeline that jumps backward, which erodes trust immediately.
5. Every carrier outage becomes yours
If tracking data is only ever fetched live at the moment a customer looks, then a carrier’s own infrastructure problem — a scheduled maintenance window, an unexpected API outage, a regional network issue — becomes visible directly to the retailer’s customers as a broken or blank tracking page, even though the retailer had nothing to do with the underlying cause. Owning a normalized, cached copy of shipment status decouples the customer experience from any single carrier’s momentary reliability, which matters enormously given that a retailer typically has zero ability to influence how quickly an external carrier resolves its own infrastructure issues.
6. Support and analytics need the data
Owning a normalized, queryable copy of shipment history lets customer support answer “where is my order” questions without cross-referencing five carrier portals, and lets analytics measure delivery performance across carriers for negotiation and routing decisions, both impossible without a unified in-house dataset.
Build a system that ingests shipment status data from many heterogeneous third-party carriers — through webhooks where available and polling where not — normalizes it into one consistent status model, resolves out-of-order and duplicate events correctly, and serves a fast, accurate, near-real-time tracking timeline to customers at very high read volume, while remaining resilient to individual carrier outages, rate limits, and API inconsistencies.
“Why not just redirect the customer to the carrier’s own tracking page instead of building this ourselves?” A good answer notes that a unified experience matters for brand trust and reduces support burden — customers do not want to learn ten different carrier websites — and that owning the data also lets the retailer proactively notify customers, feed delivery estimates back into other systems like customer support and returns, and maintain a consistent look and feel regardless of which carrier is fulfilling a given order.
Core Concepts You Need First
Before diagrams and code, here is the shared vocabulary this guide relies on, explained in plain language with a simple example for each.
Tracking number and carrier code
A tracking number is the unique identifier a carrier assigns to one shipment. Because tracking number formats overlap across carriers, the system also needs a carrier code alongside it — the pair (carrier code, tracking number), not the tracking number alone, is the true unique key used throughout the system.
Canonical status model
A small, fixed set of statuses that every carrier-specific status gets mapped into, such as LABEL_CREATED, PICKED_UP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION, and RETURNED. This canonical model is what the customer-facing UI, notification system, and analytics all build on, completely insulated from carrier-specific quirks.
Think of the canonical status model like a universal translator. No matter which language (carrier) is speaking, everything gets translated into one shared language before anyone downstream has to listen to it.
Scan event
A single, timestamped data point representing something that happened to a package — a barcode scan at a sorting facility, a handoff to a delivery driver, a proof-of-delivery capture. A shipment’s full history is an ordered sequence of scan events, and the “current status” shown to a customer is simply a summary of the most recent, most authoritative scan event received so far.
Webhook versus polling
A webhook is the carrier proactively pushing an update to a URL the retailer registered in advance, the moment something changes. Polling is the retailer’s system proactively asking the carrier’s API, on some schedule, “has anything changed for this tracking number?” Webhooks are more efficient and closer to real time, but not universally supported or fully reliable, which is why production systems use both together, covered in detail in the architecture section.
Milestone versus intermediate event
Not every scan event is equally important to a customer. A milestone event — picked up, out for delivery, delivered, exception — is significant enough to trigger a customer notification and a visible timeline update. An intermediate event — an internal hub scan, a sort facility pass-through — is useful for internal analytics and detailed tracking history but usually does not warrant interrupting the customer with a push notification.
Estimated delivery date (EDD)
A continuously refined prediction of when a package will arrive, computed initially from the carrier’s own service-level commitment and refined over the shipment’s lifetime using actual scan events and historical transit-time data for that specific lane and carrier combination.
| Term | What it answers | Typical data source |
|---|---|---|
| Scan event | What just happened to this package? | Carrier webhook or polling response |
| Canonical status | What should the customer see right now? | Mapped from carrier-specific status codes |
| Estimated delivery date | When will it likely arrive? | Carrier SLA plus historical transit-time model |
| Carrier code | Which carrier is handling this leg? | Order and shipment metadata at label creation |
Multi-leg shipments
A single order can involve more than one carrier across its journey — for example, an international seller might use one carrier for the cross-border leg and hand the package to a different regional carrier for last-mile delivery. The system needs to represent a shipment as a sequence of one or more legs, each potentially tracked by a different carrier under a different tracking number, while still presenting the customer with one continuous timeline.
Status mapping table
The concrete artifact that makes normalization possible is a configuration table, maintained per carrier, that maps every raw status code that carrier is known to emit into one canonical status. A small illustrative slice looks like the table below; in a real system this table typically holds many dozens of entries per carrier once every observed status code, including rare exception and delay codes, has been accounted for.
| Carrier | Raw status code | Canonical status |
|---|---|---|
| FedEx | PU | PICKED_UP |
| FedEx | OC | IN_TRANSIT |
| FedEx | OD | OUT_FOR_DELIVERY |
| UPS | I | IN_TRANSIT |
| UPS | D | DELIVERED |
| DHL | WC | EXCEPTION |
Terminal versus non-terminal status
A terminal status, such as DELIVERED or RETURNED, means the shipment’s journey is complete and no further meaningful updates are expected; the Polling Scheduler removes such shipments from active rotation, keeping only a brief grace-period check afterward for late correction events. Every other status is non-terminal and remains eligible for active polling and further webhook-driven updates until it eventually reaches a terminal state.
Quick-reference glossary
The following table condenses every recurring term used throughout this guide, useful as an interview refresher.
| Term | Plain-language definition |
|---|---|
| Carrier | A shipping company such as FedEx, UPS, DHL, or a regional courier that physically moves a package. |
| Scan event | A single timestamped record of something that happened to a package at a specific point. |
| Canonical status | The unified, carrier-independent status shown to customers, mapped from many different carrier-specific codes. |
| Webhook | A carrier proactively pushing an update to a URL the retailer registered in advance. |
| Polling | The retailer’s system proactively asking a carrier’s API whether anything has changed. |
| Adapter | A component that translates one carrier’s specific API shape into a common internal format. |
| Idempotency | A property where processing the same event more than once has the same effect as processing it once. |
| Multi-leg shipment | A shipment handled by more than one carrier across different segments of its journey. |
| Estimated delivery date | A continuously refined prediction of when a shipment will arrive. |
| Circuit breaker | A pattern that stops calling a failing dependency temporarily, falling back to a safe default instead. |
| Consumer lag | How far behind a message queue consumer is from the latest message produced. |
| Bloom filter | A compact, probabilistic data structure used for cheap “definitely not seen before” checks. |
System Architecture & Components
With shared vocabulary established, here is the full picture: the client-facing read path that serves a tracking status to a customer, and the ingestion pipeline that continuously pulls in data from many third-party carriers.
Below is what each labelled box in that diagram is actually responsible for, in plain terms.
CDN
Caches static assets close to the customer so only the dynamic tracking status request itself travels to the origin.
API Gateway
The single entry point for every client and support-tool request. Handles authentication, per-client rate limiting, and routing to the correct backend service.
Load Balancer
Distributes tracking-status requests across many identical Tracking Service instances using health checks, avoiding any single point of failure.
Tracking Service
The core read-path service; given a shipment or order ID, returns the current canonical status and timeline, reading from cache first.
Webhook Gateway
A dedicated, publicly reachable endpoint that receives inbound push notifications from carriers that support webhooks.
Polling Scheduler
Actively calls carrier APIs on a schedule for carriers without webhook support, or as a reliability backstop for carriers that do.
Carrier Adapters
One adapter per carrier, translating that carrier’s specific API shape and status vocabulary into a common internal event format.
Normalization Service
Maps carrier-specific raw statuses into the canonical status model and resolves ordering and deduplication before writing final state.
ETA Service
Continuously refines the estimated delivery date using carrier SLAs and historical transit-time data.
Notification Service
Sends push, email, or SMS notifications to customers when a shipment reaches a customer-relevant milestone status.
Redis Cache
Stores the current status for actively-viewed shipments so read traffic almost never has to hit the database directly.
Kafka Event Bus
Carries every raw carrier event from ingestion through to normalization and analytics asynchronously and durably.
“Why do we need both an API Gateway and a Load Balancer here — isn’t that redundant?” No. The API Gateway is the application-layer front door, handling authentication, request validation, and routing across many different backend services, including ones unrelated to tracking, such as order management and returns. The Load Balancer sits specifically in front of the Tracking Service fleet, distributing load across many stateless instances of that one service for scalability and fault tolerance. They solve different problems at different layers, and most production deployments use both together rather than treating either as a substitute for the other.
Networking considerations
Services within the Tracking Core and Carrier Integration layers communicate over a private virtual network, isolated from the public internet, except for the Webhook Gateway, which by necessity must be publicly reachable so external carrier systems can call it. Connection pooling matters heavily on the outbound side: the Polling Scheduler and each carrier adapter maintain persistent, pooled connections to their respective carrier APIs rather than establishing a fresh TLS handshake on every call, since that overhead compounds quickly at the polling volume this system sustains. Internally, service discovery lets the Tracking Service, Normalization Service, and Notification Service locate healthy instances of their dependencies dynamically as the fleet scales, rather than relying on static, manually maintained address lists.
Why the Webhook Gateway is a separate component from the Tracking Service
It might seem simpler to have carriers push updates directly into the same service that serves customer reads, but separating the two is deliberate. The Webhook Gateway needs to be defensive by design, since it accepts unauthenticated-until-verified traffic from the public internet, while the Tracking Service should remain a simple, trusted, internal-facing read path with no public write surface at all. Keeping these concerns in separate services also means a spike in inbound webhook traffic, or even an attempted abuse pattern against the webhook endpoint, cannot degrade the latency customers experience when checking their own order status.
Internal Working: From Carrier Scan to Customer Screen
It helps to separate this system into two paths that run at very different speeds and in different directions: the ingestion path, which pulls raw events in from external carriers, and the read path, which serves an already-normalized status to a customer in milliseconds.
The ingestion path — getting carrier data in
Carrier data arrives through two parallel mechanisms. Carriers that support webhooks push an event to the Webhook Gateway the moment a scan happens at their end; carriers that do not, or as a reliability backstop for carriers that sometimes miss a webhook delivery, are covered by the Polling Scheduler, which calls each carrier’s API on a schedule tuned to that shipment’s current status — a shipment marked out for delivery is polled far more frequently than one that has been sitting in the same overseas customs facility for a week. Either way, the raw carrier-specific event lands on the Kafka event bus in its original shape, tagged with which carrier and adapter produced it.
The normalization path — making sense of it
The Stream Processor consumes each raw event, and the Normalization Service maps the carrier-specific status code into the canonical status model described earlier, resolves whether this event is newer or older than the shipment’s current recorded state, and deduplicates it against events already processed. Only after this resolution does the service write the new canonical state to the database and update the cache, and only if the event represents a genuine milestone does it trigger the Notification Service.
The read path — serving a status fast
When a customer opens a tracking page, the request never touches any carrier system directly. The Tracking Service looks up the already-normalized, already-resolved status sitting in the Redis cache, falling back to the database on a cache miss. This is the same cache-first pattern used broadly across high-read-volume systems, and it is what keeps tracking page load times fast and independent of any external carrier’s availability or latency.
Here is a simplified Java implementation of the core normalization and ordering logic, written the way it might appear inside the Normalization Service.
public class ShipmentEventProcessor {
public void process(RawCarrierEvent event) {
CanonicalStatus mapped = statusMapper.map(event.getCarrierCode(), event.getRawStatus());
ShipmentRecord current = shipmentRepository.find(event.getCarrierCode(), event.getTrackingNumber());
if (isDuplicate(current, event)) {
return; // already processed this exact scan event, ignore
}
if (isOutOfOrder(current, event)) {
eventHistoryRepository.append(event); // keep for audit, do not update "current" status
return;
}
ShipmentRecord updated = current.withStatus(mapped, event.getScanTimestamp());
shipmentRepository.save(updated);
cache.put(cacheKey(event), updated, DEFAULT_TTL);
if (mapped.isCustomerFacingMilestone()) {
notificationService.notifyMilestone(updated);
}
}
private boolean isDuplicate(ShipmentRecord current, RawCarrierEvent event) {
return current != null && current.getLastEventId().equals(event.getEventId());
}
private boolean isOutOfOrder(ShipmentRecord current, RawCarrierEvent event) {
return current != null && event.getScanTimestamp().isBefore(current.getLastScanTimestamp());
}
}
The Normalization Service works like an air traffic controller reading radar blips from several different radar stations that each report in a slightly different format. The controller does not just display whichever blip arrived most recently on the screen; they cross-check timestamps and known aircraft positions first, so a stale or duplicate blip never causes the displayed position to jump backward.
“A ‘delivered’ webhook and an ‘out for delivery’ webhook for the same package arrive almost simultaneously, but out of order. How do you make sure the customer never sees the status jump backward from delivered to out for delivery?” A strong answer explains that every incoming event is compared against the shipment’s current recorded scan timestamp, not just accepted blindly in arrival order; an event whose timestamp is older than what is already stored is kept for the audit history but never allowed to overwrite the current customer-facing status, which is exactly the ordering check shown in the code above.
A complete worked example
It helps to trace one shipment end to end. Suppose a package ships with FedEx, and its label is created at 2:00 PM. At 6:30 PM the same day, FedEx’s system sends a pickup-scan webhook, which the Webhook Gateway receives, verifies, and publishes to Kafka within milliseconds; the Normalization Service maps FedEx’s raw "PU" code to the canonical PICKED_UP status, confirms this timestamp is newer than the shipment’s current state, and updates both the database and cache, after which the Notification Service sends the customer a short “your order has shipped” push notification.
Overnight, the package passes through two intermediate sorting facilities. FedEx does not send webhooks for these intermediate scans, but the Polling Scheduler, checking this actively-moving shipment every fifteen minutes, picks up both scan events on its next two poll cycles and appends them to the full scan history; since neither is a customer-facing milestone, no notification fires, and the canonical status simply updates to IN_TRANSIT with a refreshed location. The following morning, a genuine milestone webhook arrives for "out for delivery," triggering both a status update and a customer notification, and finally a proof-of-delivery webhook arrives that afternoon, transitioning the shipment to its terminal DELIVERED state and removing it from the active polling rotation entirely. This same sequence, repeated automatically across millions of concurrently in-flight shipments, is the entire system in miniature.
Algorithms, data structures & concurrency
A handful of classical techniques do most of the heavy lifting underneath the service boundaries already described, and understanding them makes it much easier to reason about correctness under the kind of out-of-order, duplicate-prone data this system constantly deals with.
Idempotency keys and deduplication
Every carrier event, whether delivered by webhook or polling, is assigned or carries a stable event identifier. The system keeps a short-lived deduplication index — often a Redis set with a time-to-live long enough to cover realistic retry windows — keyed by that identifier, so a webhook redelivered after a timeout, or a polling cycle that happens to re-fetch an already-seen event, is recognized and dropped before it can trigger duplicate processing or a duplicate customer notification.
Vector-clock-style ordering without a full vector clock
Strictly speaking, safely ordering events from multiple independent, loosely synchronized carrier systems is a classic distributed-systems ordering problem, the kind vector clocks were designed to solve in full generality. In practice, most shipment tracking systems use a simpler, pragmatic approximation: they trust each carrier’s own reported scan timestamp as the ordering key, since carriers already timestamp their own scans reliably, and only fall back to arrival-order tie-breaking when two events from the same carrier report the exact same timestamp. This is a deliberate simplification that trades theoretical rigor for practical simplicity, which is a completely reasonable choice once the business impact of a rare ordering ambiguity is well understood and bounded.
Priority queue for the polling scheduler
The Polling Scheduler cannot afford to poll every shipment at the same fixed interval — a shipment marked out for delivery genuinely needs checking every few minutes, while a shipment sitting untouched in the same status for days can be checked far less often. A priority queue, ordered by “next scheduled poll time,” lets the scheduler always pop the shipment most due for a check next, in roughly logarithmic time, and reschedule it further out or closer in based on its current status after each poll.
Bloom filter for cheap “have we seen this before” checks
Before even hitting the full deduplication index, some high-throughput ingestion pipelines place a Bloom filter in front of it — a compact, probabilistic data structure that can say “definitely not seen before” very cheaply, and only falls through to the more expensive exact check when it says “possibly seen before.” Because most incoming events genuinely are new, this cuts the load on the exact deduplication store substantially, at the cost of a small, tunable false-positive rate that never causes incorrect behavior, only an occasional unnecessary exact check.
Concurrency control on the shipment record
Because webhook delivery and polling can occasionally produce two near-simultaneous events for the same shipment, writes to a shipment’s record use optimistic concurrency control with a version number, exactly as described for other high-write-contention systems: a writer reads the current version, applies its update, and writes back only if the version has not changed, retrying with the freshest state if it has. This avoids holding a lock for the duration of the normalization logic, which matters at the scale of millions of concurrently in-flight shipments.
public class OptimisticShipmentWriter {
public boolean applyUpdate(String shipmentKey, ShipmentRecord newState, long expectedVersion) {
int rowsUpdated = shipmentRepository.updateIfVersionMatches(
shipmentKey, newState, expectedVersion, expectedVersion + 1);
return rowsUpdated == 1; // false means a concurrent writer won; caller re-reads and retries
}
}
“How would you avoid re-polling a carrier for a shipment that already reported ‘delivered’ an hour ago?” The scheduler should remove a shipment from the active polling rotation once it reaches a terminal canonical status such as delivered, returned, or cancelled, keeping only a short grace-period check afterward in case a late correction event arrives, rather than continuing to poll indefinitely and wasting carrier API quota on a shipment whose journey is already complete.
Data Flow & Lifecycle
Tracing one update from a carrier scan all the way to a customer’s screen is one of the most common whiteboard exercises for this kind of system. Two views are useful: the sequence for an inbound webhook event, and the lifecycle a shipment record moves through over its life.
Separately, every shipment record itself moves through a well-defined lifecycle, independent of which specific carrier or how many legs are involved. Representing this explicitly as a state machine keeps the Notification Service and the Polling Scheduler both easy to reason about, since each transition has clear, well-understood implications for what should happen next.
Notice that the ingestion side of this pipeline is entirely event-driven and asynchronous, exactly like the normalization logic described in the previous section. Raw events land on Kafka, get consumed and normalized by the Stream Processor, and only meaningful, order-verified transitions ever reach the database, cache, and Notification Service. This separation is what keeps a burst of duplicate or out-of-order carrier events from ever becoming visible to a customer.
Event schema and partitioning
Each raw carrier event published to Kafka carries a compact schema — carrier code, tracking number, raw status code, scan timestamp, event identifier, and a small location payload — and is published keyed by the combination of carrier code and tracking number. This guarantees that every event for the same shipment lands in the same Kafka partition and is therefore processed by the Stream Processor in the order it was produced, which is exactly the ordering guarantee the normalization logic’s timestamp comparison depends on to behave correctly.
{
"eventId": "evt-7c19a2",
"carrierCode": "FEDEX",
"trackingNumber": "784523901823",
"rawStatus": "OC",
"scanTimestamp": "2026-07-30T08:41:10Z",
"location": "Memphis, TN"
}
Backpressure and consumer lag
During an unusually large event burst — for instance, a major shipping-season peak day driving a flood of pickup and hub-scan events across the entire fleet simultaneously — the Stream Processor can temporarily fall behind the rate at which events are being produced. Kafka’s design tolerates this gracefully: events queue durably on the broker rather than being dropped, and the consumer catches up once the burst subsides, at the cost of a temporary increase in consumer lag. This is exactly why consumer lag is tracked as a first-class monitoring metric, since a rising lag is often the earliest warning sign that the normalization pipeline needs more processing capacity well before customers would notice any delay in their tracking page.
Advantages, Disadvantages & Trade-offs
Every architectural choice in a shipment tracking platform trades one property against another. The most important trade-offs are worth naming directly so that the design can be tuned to the specific business context rather than defended by inertia.
Advantages
- Gives customers one consistent tracking experience regardless of carrier.
- Reduces support burden through proactive milestone notifications.
- Gives the business its own queryable shipment data for analytics, carrier negotiations, and returns handling.
- Decouples customer experience from individual carrier availability, latency, and maintenance windows.
Disadvantages
- Adds real integration and maintenance overhead per carrier.
- Depends heavily on external systems the business does not control.
- Requires ongoing effort to keep status mappings accurate as carriers change their own APIs.
- Introduces a non-trivial storage and streaming bill that a naive per-request pass-through would avoid.
Freshness versus API cost
The key trade-off is freshness versus API cost: polling more frequently produces fresher status but consumes more of each carrier’s rate-limited quota, which is finite and shared across the entire shipment volume. Intelligent prioritization — polling out-for-delivery shipments frequently and dormant ones rarely — is what makes this trade-off manageable at scale.
Consistency versus availability
Another trade-off worth naming directly is consistency versus availability, the same CAP theorem tension seen throughout distributed systems. When the shipment database or a specific carrier’s data is temporarily unreachable, the system can either refuse to show a status until it can guarantee correctness, or show the last known cached status even though it might be a few minutes stale. Nearly every production tracking system chooses availability here, since a customer seeing a slightly stale status is a far smaller problem than a customer seeing a broken page during a partial outage.
Granularity versus signal quality
A further trade-off exists between granularity and signal quality. Surfacing every single intermediate scan event to the customer feels maximally transparent, but in practice it often produces a confusing, noisy timeline full of internal facility codes that mean nothing to a shopper. Curating the timeline down to genuinely meaningful milestones produces a cleaner experience at the cost of hiding detail that a small subset of detail-oriented customers might actually want, which is why many systems offer the curated view by default with an option to expand into the full raw scan history.
Build versus buy
Finally, integrating each carrier in-house versus subscribing to a dedicated shipment-tracking aggregator is a straightforward build-versus-buy decision. Building in-house yields deep control, lower per-shipment marginal cost at very high volume, and direct access to raw data; buying from an aggregator gives immediate access to hundreds of carriers, offloads the ongoing maintenance burden of carrier API changes, and is typically the right choice for smaller retailers or teams entering new geographies quickly.
Performance & Scalability
The read path and the ingestion path scale along very different dimensions, and it is worth reasoning about each separately.
Scaling the read path
Because reads are cache-first and the Tracking Service instances are stateless, horizontal scaling is straightforward: add more instances behind the load balancer and more Redis replicas as traffic grows. A well-tuned deployment can serve the large majority of tracking-status lookups directly from cache with p99 latency in the low tens of milliseconds, since the dominant cost is simply the Redis round trip plus network overhead.
Scaling the ingestion path
The ingestion side bottleneck is usually outbound API capacity, not internal compute: nearly every carrier enforces a rate limit on how many polling calls can be made per minute, and exceeding it risks temporary suspension of API access entirely. This makes intelligent poll scheduling — checking active, high-priority shipments frequently and dormant ones rarely — a direct performance requirement, not just a cost optimization, since a naive fixed-interval poll of every shipment would exhaust rate limits long before covering a large active shipment volume.
Batching and connection reuse
Where a carrier’s API supports it, the Polling Scheduler batches multiple tracking numbers into a single API call rather than issuing one call per shipment, which dramatically improves both throughput and rate-limit efficiency. Persistent, pooled connections to each carrier’s API also avoid the overhead of establishing a fresh TLS handshake on every single polling call, which matters at the volume this system operates at.
Handling shipping-season traffic spikes
Major shopping seasons can push both shipment volume and tracking-page read traffic up by an order of magnitude for a period of weeks. Autoscaling policies for the Tracking Service and Webhook Gateway, pre-warmed cache capacity, and temporarily relaxed poll intervals for low-priority shipments all help the system absorb this predictable seasonal load without a proportional increase in steady-state infrastructure spend the rest of the year.
“One carrier’s API starts responding ten times slower than normal during a peak shipping week. How do you stop that from degrading tracking accuracy for shipments on other carriers?” A strong answer combines bulkhead isolation, so the slow carrier’s adapter cannot exhaust shared thread or connection pools, with a circuit breaker that temporarily reduces polling frequency or pauses non-critical polling for that carrier specifically, while webhook ingestion for other carriers and the customer-facing read path continue operating normally and completely unaffected.
Cost optimization
At the scale this system runs, infrastructure and API cost both become genuine design constraints. Prioritized polling, as already described, means the majority of dormant or terminal-state shipments consume almost no ongoing compute or API quota, since only actively moving shipments are polled frequently. Batching polling calls where a carrier’s API allows it improves both throughput and cost efficiency, since the fixed overhead of an API call is amortized across many tracking numbers rather than paid once per shipment. Right-sizing the cache TTL matters here too: too short a TTL forces unnecessary database reads, while too long a TTL risks serving a status that is more stale than customers expect, and most teams tune this value empirically per shipment status, since a shipment out for delivery benefits from a much shorter TTL than one sitting in a stable in-transit state.
Capacity planning example
As a concrete illustration, consider a retailer with three million active shipments at any given time, of which roughly four hundred thousand are in a fast-moving state such as out for delivery and require polling every few minutes as a webhook backstop, while the remaining shipments are either covered entirely by webhooks or sit in a slow-moving state polled only once every few hours. The fast-moving tier alone requires on the order of a few thousand polling calls per minute sustained across all carriers combined, which is comfortably achievable within typical carrier rate limits when spread across many carrier accounts, whereas polling the entire three million shipment volume at the same frequency would exceed most carriers’ rate limits many times over for no meaningful gain in tracking accuracy for shipments that are not actively moving.
High Availability & Reliability
A tracking system failure is highly visible to customers at exactly the moment they are anxious about an order, which makes reliability here a genuine product concern, not just an engineering one.
Redundancy at every layer
Every component — API Gateway, Load Balancer, Tracking Service instances, Webhook Gateway, Redis, and the shipment database — runs as multiple redundant nodes spread across at least three availability zones, so no single machine or zone failure can take tracking availability down.
Graceful degradation per carrier
If one carrier’s API becomes fully unavailable, only shipments actively handled by that specific carrier are affected, and even then, the system continues serving the last known good status from cache and database rather than showing an error, while the circuit breaker described earlier prevents the outage from cascading into the shared polling infrastructure used by other carriers.
Webhook delivery reliability
Because webhook delivery is never fully guaranteed by the sending carrier, the Webhook Gateway acknowledges receipt quickly and durably queues the event before any processing happens, so a downstream processing failure never causes an accepted webhook to be silently lost. The polling backstop described earlier provides a second layer of protection specifically against webhooks that the carrier believes it sent but that never actually arrived.
Data replication and backup
The shipment database replicates across zones so a single node failure does not lose recent status updates, and periodic backups combined with write-ahead-log shipping allow point-in-time recovery if a bad deployment or bug corrupts data across a slice of shipments. As with other systems in this space, a brief window of eventual consistency during a rare failover is an acceptable trade-off, since a shipment status that is a few minutes stale is a far smaller problem than an unavailable tracking page.
Failure recovery in the event pipeline
If the Stream Processor crashes mid-window, Kafka’s committed consumer offsets mean the replacement instance resumes exactly where processing left off rather than losing track of progress, and the idempotency and deduplication logic described earlier ensures that any events reprocessed as part of that recovery produce the same final state as if no crash had occurred.
This is like a relay race with a backup runner standing ready at every handoff point. If one runner stumbles, the backup steps in immediately from exactly where the baton was dropped, rather than the whole race having to restart from the beginning.
Chaos testing
Teams operating this kind of system at scale often deliberately inject failures into staging or even production — killing a Tracking Service instance mid-request, simulating a specific carrier’s API timing out entirely, or forcing a database failover — specifically to verify that the fallback, circuit-breaker, and redundancy mechanisms described in this section actually behave as designed under real conditions, rather than only working in theory. This practice catches gaps between the intended failure-handling design and its real implementation well before an actual carrier outage does, which given how many external dependencies this system has, is a matter of when, not if.
Security
Because this system exchanges data with many external carrier systems and exposes a public webhook endpoint, it faces some security concerns beyond the standard list.
- Webhook signature verification — every inbound webhook must be cryptographically verified against the sending carrier’s published signing scheme before its payload is trusted, since the Webhook Gateway’s URL is, by necessity, publicly reachable and therefore a plausible target for spoofed payloads.
- Rate limiting on the webhook endpoint — even legitimate carriers can occasionally misbehave and flood an endpoint with retries; the Webhook Gateway applies per-carrier rate limiting and request size limits to protect itself from both accidental floods and malicious traffic.
- Least-privilege carrier credentials — API credentials for each carrier’s polling API are scoped as narrowly as that carrier’s platform allows and stored in a dedicated secrets manager with automatic rotation, never embedded in adapter configuration files.
- Customer data minimization — the public tracking API and any customer-facing timeline expose only what a customer needs to see; internal fields such as full carrier facility codes or internal routing metadata stay internal.
- Encryption in transit and at rest — standard TLS for every API and webhook call, and encryption at rest for the shipment database given that delivery addresses and customer identifiers pass through this system.
Accepting webhook payloads without signature verification because “the URL is hard to guess.” An unguessable URL is not authentication; any leaked or logged URL becomes a spoofing vector the moment signature verification is skipped.
“How would you prevent a malicious actor from spoofing a fake ‘delivered’ webhook to make a stolen package look successfully delivered?” The answer centers on strict signature verification tied to each carrier’s actual signing key, combined with sanity checks such as verifying the event’s claimed scan location and timestamp are plausible given the shipment’s known recent history, and flagging anomalous transitions — such as jumping straight from label-created to delivered with no intermediate scans — for manual review rather than accepting them silently.
Secrets and credential management
Each carrier adapter depends on API credentials specific to that carrier, and the Webhook Gateway depends on signing secrets used to verify inbound payloads. None of these should live in application configuration files or environment variables checked into source control; instead, a dedicated secrets manager issues short-lived, automatically rotated credentials to each adapter at runtime, so a leaked configuration file or compromised container does not expose a long-lived, high-privilege credential for an external carrier account.
Protecting the webhook endpoint from denial-of-service traffic
Because the Webhook Gateway is necessarily public-facing, it is a plausible target for a denial-of-service attempt aimed at degrading ingestion or, worse, at overwhelming downstream services through a flood of junk payloads. Layered defenses matter: the API Gateway and any upstream CDN filter obviously malicious traffic before it reaches the Webhook Gateway itself, strict payload size limits and per-source rate limiting cap how much any single sender can push through, and the gateway’s fast acknowledge-then-queue pattern, described earlier under reliability, means even a legitimate traffic surge from a carrier during a peak shipping day cannot cause request processing to back up in a way that risks timeouts or dropped events.
Monitoring, Logging & Metrics
A shipment tracking platform touches enough external systems that observability is not optional — it is what turns a silent, cascading external failure into an actionable alert.
Key metrics to track
| Metric | Why it matters |
|---|---|
| Tracking API latency (p50, p95, p99) | Directly affects customer-facing page load time |
| Cache hit ratio | Low hit ratio signals rising database load ahead of time |
| Webhook delivery success rate per carrier | Detects a carrier silently failing to deliver updates |
| Polling success and rate-limit rejection rate | Signals a carrier integration nearing or exceeding its quota |
| Kafka consumer lag | Rising lag means normalization is falling behind real time |
| Out-of-order and duplicate event rate | Sudden spikes can indicate a carrier-side bug or clock skew |
Logging and tracing
Every processed event logs the inputs that produced its outcome — raw carrier status, mapped canonical status, and whether it was accepted, treated as a duplicate, or flagged as out of order — which is essential both for debugging and for explaining a specific shipment’s history to a customer support agent. Distributed tracing ties a single webhook event’s journey together across the Webhook Gateway, Kafka, Stream Processor, and Normalization Service, which is invaluable when diagnosing why a specific shipment’s status update took longer than expected to appear.
Per-carrier health dashboards
Because this system’s reliability depends heavily on external parties, dashboards are typically organized per carrier, not just per internal service, tracking each carrier’s webhook delivery rate, polling success rate, and average event latency independently. This makes it immediately obvious when a specific carrier partner is degrading, which is often the very first sign of a problem, well before it would show up as a general system-health alert.
Monitoring this system is like a shipping port’s control tower tracking each incoming vessel line separately, rather than just watching one combined average — a single struggling shipping line shows up immediately on its own dedicated readout, long before it would move the port-wide average enough to notice.
A typical debugging workflow
When a customer support ticket says “my tracking status looks wrong,” the on-call engineer’s first step is usually pulling up that shipment’s full scan history and audit log, which shows every raw event received, its mapped canonical status, and whether it was accepted, treated as a duplicate, or flagged as out of order. If the history shows an event that should have updated the status but did not, the next step is checking the Normalization Service’s mapping configuration for that carrier and raw status code, since a missing or incorrect mapping entry is a far more common root cause than a bug in the ordering logic itself. Distributed tracing then lets the engineer follow one specific event across the Webhook Gateway, Kafka, Stream Processor, and Normalization Service, spotting exactly where it diverged from the expected path. This log-first, mapping-second debugging order resolves the overwhelming majority of tracking discrepancies quickly, without needing to reason abstractly about the pipeline as a whole.
Deployment & Cloud
Modern shipment tracking systems are typically deployed on containers orchestrated by Kubernetes across multiple availability zones on a major cloud provider. A few practices matter specifically for this domain.
- Canary releases for adapter changes — a change to a carrier adapter, such as a new status-mapping update, is rolled out to a small percentage of that carrier’s traffic first, with close monitoring of normalization error rates, before a full rollout.
- Blue-green deployment for the Tracking Service — a full parallel environment is stood up and traffic is switched over only once health checks pass, allowing instant rollback.
- Infrastructure as code — Kubernetes manifests, Kafka topic configuration, and autoscaling policies are defined declaratively so environments are reproducible and auditable.
- Managed services where they reduce risk — many teams choose managed Kafka, managed Redis, and a managed NoSQL database rather than operating these themselves.
Testing and validation before release
Because a bug in a carrier’s status-mapping table can quietly mislabel thousands of shipments before anyone notices, changes to the Normalization Service are validated against a large library of recorded real carrier payloads in a staging environment before deployment, and shadow-deployed against live production traffic — processing real events but writing to a separate, non-customer-facing table — to compare outputs against the current production mapping before the change is ever promoted to a canary rollout.
Multi-region operation
A retailer operating across several regions typically deploys a full regional copy of this architecture, since carrier partnerships, webhook endpoints, and even which carriers are available at all can differ significantly by geography. A thin shared layer above the regional deployments handles any global reporting that needs to aggregate shipment data across regions for leadership visibility.
Databases, Caching & Load Balancing
The data layer directly determines both the read-path latency customers experience and the ingestion pipeline’s ability to keep up with bursty carrier events.
Choosing the shipment database
The shipment database is the durable source of truth for every shipment’s current canonical status and full scan history. Because a large retailer can have tens of millions of active or recently completed shipments at once, this store is typically a horizontally partitioned, wide-column or document database such as Cassandra or DynamoDB, partitioned by the combination of carrier code and tracking number, since the dominant access pattern is a simple key-based lookup or append rather than complex relational joins.
The full scan history for a shipment — every intermediate hub scan, not just the customer-facing milestones — is a natural fit for an append-only event table, since scan events are immutable facts that are never updated after the fact, only ever added to. Keeping this history separate from the “current status” summary record lets each be optimized differently: the current-status record is read constantly and needs to be extremely fast, while the full history is read far less often, mostly for support investigations and analytics.
Why caching is not optional here either
A retailer serving hundreds of thousands of tracking page views per minute during a peak shipping season cannot afford to send every one of those reads to the database. The Tracking Service keeps the current canonical status for actively-tracked shipments in a Redis cache with a short time-to-live, populated and refreshed by the Normalization Service the moment a genuine status change is confirmed, using the same write-through pattern used broadly in high-read-volume systems: whenever the source of truth changes, the cache is updated in the same operation rather than waiting for a natural expiry.
The cache here works like an arrivals board at an airport, updated the instant new flight data comes in from air traffic control, so passengers glancing at the board never have to wait for someone to manually walk over and check with the tower.
Load balancing across tracking service instances
The Tracking Service runs as a fleet of stateless instances behind a Layer 7 load balancer, exactly as described for the read path in general. Because no instance holds any session-specific state — every instance can read the same cache and database — the fleet scales horizontally simply by adding more instances, and health checks let the load balancer stop routing to any instance that becomes unhealthy without manual intervention.
| Layer | Technology examples | Why it fits here |
|---|---|---|
| Hot status cache | Redis, Memcached | Sub-millisecond reads, TTL support, write-through friendly |
| Shipment source of truth | Cassandra, DynamoDB | Horizontally scalable, simple key-based access pattern |
| Scan event history | Append-only event store, columnar table | Immutable facts, write-heavy, infrequently read in full |
| Event backbone | Kafka, Kinesis | Ordered, partitioned, durable, high-throughput streaming |
“What happens if the cache is completely unavailable during a peak shipping period?” The Tracking Service should treat the cache purely as an optimization, falling back to reading directly from the database when it is unavailable. This should be paired with request coalescing so that many simultaneous cache-miss reads for the same popular shipment do not all independently hit the database at once, and with a circuit breaker so a struggling cache layer cannot drag down the entire read path’s latency.
Partitioning and hot shipments
Partitioning the shipment database by carrier code plus tracking number spreads load evenly under normal conditions, but a single viral event — for instance, a widely shared social media post about a delayed high-profile shipment — can occasionally cause one specific shipment to receive an unusually large share of read traffic. Because the cache absorbs the vast majority of reads regardless of database partitioning, this kind of hotspot rarely becomes a real database problem in practice, which is one more reason the cache-first read path matters as much for resilience as it does for raw speed.
Replication choices and CAP theorem trade-offs
Each shard of the shipment database is itself replicated, typically with one primary node accepting writes and several replicas serving reads and standing ready for promotion during a failover. As with the caching layer, the choice between synchronous replication, which waits for replica confirmation before acknowledging a write, and asynchronous replication, which acknowledges immediately, is a direct latency-versus-durability trade-off. Because a lost scan event is generally recoverable — the polling backstop will re-fetch it on its next cycle, and webhook retries frequently resend it anyway — most implementations favor the lower write latency of asynchronous replication, accepting a very small window of possible data loss during a rare failover.
This is also a clean illustration of the CAP theorem in practice: during a network partition between the Normalization Service and the primary database shard, the system must choose between consistency, refusing to acknowledge a write until it can guarantee durability, and availability, accepting the write locally and reconciling later. Given that a tracking status update delayed by a few seconds during a rare partition event is a far smaller problem than the ingestion pipeline stalling entirely, availability is almost always the right choice for this specific domain, exactly as it is for the caching layer decisions made earlier in this guide.
Consensus for partition ownership
When the Stream Processor runs as multiple parallel workers consuming different Kafka partitions, the cluster needs to agree on which worker owns which partition, and to reassign ownership cleanly if a worker crashes. This is handled by the underlying streaming platform’s own consensus mechanism, such as Kafka’s consumer group protocol, which is itself built on well-established consensus algorithms. Engineers building on top of Kafka rarely need to implement this coordination themselves, but understanding that it exists, and that it is what guarantees exactly one worker processes each partition at a time, is useful for reasoning about correctness under worker failures.
APIs & Microservices
Clean service boundaries let different teams own, scale, and deploy each part of this system independently. A reasonable boundary for shipment tracking looks like the following.
- Tracking Service — owns the public read API (
GET /v1/shipments/{shipmentId}) and orchestrates cache and database reads. - Webhook Gateway — owns inbound carrier callback endpoints, one per carrier, each with its own authentication scheme as required by that carrier.
- Polling Scheduler Service — owns the scheduling logic and rate-limited outbound calls to each carrier’s polling API.
- Normalization Service — owns the canonical status mapping tables and the ordering and deduplication logic.
- ETA Service — exposes an internal scoring endpoint used to refine estimated delivery dates as new scan events arrive.
- Notification Service — owns customer notification preferences and delivery across push, email, and SMS channels.
Each of these is a separate deployable service because they scale and fail independently. The Webhook Gateway needs to handle unpredictable inbound bursts from external carriers and must be defensive about malformed or malicious payloads; the Polling Scheduler is compute-light but has to respect strict per-carrier rate limits; and the Tracking Service needs to sustain enormous, latency-sensitive read volume that looks nothing like the bursty, write-heavy ingestion side.
Public API design
The externally facing API stays intentionally simple, since the storefront, mobile app, and support console all depend on this exact contract.
GET /v1/shipments/SHIP-90213
{
"shipmentId": "SHIP-90213",
"canonicalStatus": "OUT_FOR_DELIVERY",
"estimatedDelivery": "2026-07-30T18:00:00Z",
"carrier": "FEDEX",
"trackingNumber": "784523901823",
"lastUpdated": "2026-07-30T08:41:10Z",
"timeline": [
{ "status": "LABEL_CREATED", "timestamp": "2026-07-27T14:02:00Z" },
{ "status": "PICKED_UP", "timestamp": "2026-07-27T18:30:00Z" },
{ "status": "IN_TRANSIT", "timestamp": "2026-07-28T09:15:00Z" },
{ "status": "OUT_FOR_DELIVERY", "timestamp": "2026-07-30T08:41:10Z" }
]
}
Notice the response exposes only the canonical status model and a clean timeline, never the raw carrier-specific status codes or internal adapter details. This keeps the public contract stable even as carrier partnerships change, adapters get rewritten, or new carriers are onboarded, none of which should ever require a breaking change to the customer-facing API.
Internal carrier adapter interface
Every carrier adapter implements the same internal interface, regardless of how different the underlying carrier API actually is, which is what makes onboarding a new carrier mostly a matter of writing one new adapter rather than touching the rest of the system.
public interface CarrierAdapter {
String getCarrierCode();
List<RawCarrierEvent> pollShipment(String trackingNumber);
RawCarrierEvent parseWebhookPayload(String rawPayload, Map<String, String> headers);
boolean verifyWebhookSignature(String rawPayload, Map<String, String> headers);
}
“How would you onboard a brand-new carrier without touching the Tracking Service or the customer-facing API at all?” A strong answer points directly at the adapter interface shown above: implementing a new class that satisfies CarrierAdapter, adding its status-code mapping table to the Normalization Service’s configuration, and registering its webhook endpoint, if it has one, are the only changes required — the Tracking Service, cache, database schema, and public API all remain completely untouched, because they only ever operate on the canonical model.
Batch status lookups
An order summary page might need statuses for several shipments belonging to one order at once, and issuing a separate network call per shipment from the client would be wasteful. A batch endpoint accepts a list of shipment IDs and returns all of their statuses in one round trip, which the Tracking Service fulfils with a single multi-key cache lookup rather than many individual ones.
POST /v1/shipments/batch
{ "shipmentIds": ["SHIP-90213", "SHIP-90214"] }
{
"shipments": [
{ "shipmentId": "SHIP-90213", "canonicalStatus": "OUT_FOR_DELIVERY" },
{ "shipmentId": "SHIP-90214", "canonicalStatus": "DELIVERED" }
]
}
Versioning and backward compatibility
Because the storefront, mobile app, and any partner integrations all consume this API and cannot all be updated simultaneously, the API is versioned explicitly in its path, and new fields are always added in a backward-compatible way. A change such as adding customs-clearance details for international shipments should never break an existing integration that only understands the original domestic timeline shape; only a genuinely breaking change justifies a new version number and a formal deprecation timeline for the previous one.
Idempotency on internal write endpoints
The internal endpoint the Normalization Service uses to apply a status update accepts an idempotency key derived from the source event’s identifier. If a retry or a reprocessed event after a crash recovery resends the same update, the service recognizes the repeated key and returns the original result rather than reapplying it, which is the same idempotency guarantee described earlier in the algorithms section, now expressed as a concrete API contract.
Design Patterns & Anti-Patterns
A short list of well-known patterns does most of the reusable design work in this system, and an equally short list of anti-patterns tends to cause most of the avoidable failures.
Patterns worth using
- Adapter pattern — the Carrier Adapter interface shown above is a textbook use of the adapter pattern, translating many incompatible external interfaces into one consistent internal contract.
- Circuit breaker — wraps every outbound call from the Polling Scheduler to a carrier’s API. If a specific carrier starts failing or timing out, the breaker trips and that carrier’s shipments are temporarily excluded from active polling rather than repeatedly retried into a failing endpoint.
- Bulkhead isolation — gives each carrier adapter its own connection pool and thread pool, so a slow or degraded carrier cannot exhaust resources needed to poll or process events from healthy carriers.
- Event sourcing for scan history — every scan event is stored as an immutable fact, which makes it possible to reconstruct a shipment’s full history for support investigations, disputes, and analytics, independent of what the “current status” summary happens to show.
- Strangler pattern during carrier migration — when replacing a legacy direct-integration approach with the adapter-based architecture described here, carriers can be migrated one at a time behind the same public API, with no visible change to customers.
Anti-patterns to avoid
- Hardcoding carrier-specific logic inside the Tracking Service instead of behind an adapter interface, which makes every new carrier integration a risky change to the same shared, high-traffic service.
- Trusting webhook delivery as the only ingestion path for any carrier, since webhook delivery is never fully guaranteed; a polling backstop, even at a low frequency, is what keeps the system correct when a webhook is silently dropped.
- Overwriting current status on every event without an ordering check, which is precisely how customers end up seeing a delivered shipment revert to in-transit.
- Notifying customers on every intermediate scan rather than only true milestones, which quickly trains customers to ignore or disable notifications entirely.
Treating a carrier’s polling API as a real-time source. Polling always has a latency floor determined by the polling interval; only webhooks approach true real time, and even they have delivery latency. Set customer expectations and internal SLAs accordingly.
The saga pattern for multi-step milestone processing
Applying a milestone update sometimes involves several coordinated steps — writing the new canonical status, updating the cache, appending to the audit history, and triggering a customer notification — each owned by a different part of the system. A saga breaks this into a sequence of local steps with defined compensating actions: if the database write succeeds but the cache update fails, the compensating action simply invalidates the stale cache entry on next read rather than leaving the system in a state where the database and cache permanently disagree, which is generally simpler and more resilient than attempting a single distributed transaction across independently owned services.
Read-through and write-through caching, named precisely
The caching strategy used throughout this system combines two named patterns explicitly. Read-through caching means the Tracking Service itself is responsible for fetching from the database on a cache miss and populating the cache, so callers never need to know the cache exists. Write-through caching means the Normalization Service updates the cache in the same operation as the database write, rather than relying solely on a time-based expiry to eventually pick up the change. Using both together is what keeps tracking status reliably fresh without depending on TTL expiry alone.
Best Practices & Common Mistakes
Distilled operational wisdom for teams building or running a shipment tracking platform in production.
Best practices
- Always compare an incoming event’s scan timestamp against the shipment’s current recorded state before applying it, never trust arrival order alone.
- Treat every carrier integration as unreliable by default, and build explicit fallback and circuit-breaker behavior for each one rather than assuming external APIs will simply stay available.
- Keep the full raw scan history even though the customer-facing timeline is curated, since support teams and analytics regularly need the complete picture.
- Design the adapter interface first, before integrating the first carrier, so every subsequent carrier onboarding follows the same well-tested pattern.
- Reserve customer notifications for genuine milestones only, and let customers control notification frequency and channel preferences.
Common mistakes
- Building carrier-specific logic directly into shared services instead of behind a consistent adapter interface.
- Relying solely on webhooks without a polling backstop, leaving the system blind whenever a webhook delivery silently fails.
- Polling every shipment at a fixed interval regardless of its current status, wasting rate-limited API quota on dormant shipments.
- Skipping webhook signature verification because the endpoint URL is assumed to be private.
- Failing to plan for multi-leg shipments, resulting in a broken or duplicated timeline whenever a package changes carriers mid-journey.
A pre-launch checklist
Before onboarding a new carrier or launching this system on a meaningful slice of real shipment volume, it is worth confirming each of the following explicitly: the new carrier’s adapter has been tested against a library of real sample payloads; its status-mapping table covers every status code the carrier is known to emit, with a sensible default for anything unmapped; webhook signature verification is in place and tested; the polling backstop is configured with a rate limit safely below the carrier’s published quota; and dashboards for that carrier’s webhook and polling health are wired up to alerting before real customer shipments start flowing through it.
Key takeaways
- Trust timestamps, not arrival order.
- Every carrier is unreliable by default.
- Keep raw history; curate the customer view.
- Design the adapter interface before the first integration.
- Notify only on milestones, not intermediate scans.
Real-World Industry Examples
Different corners of the industry apply the same core architecture in noticeably different ways. Studying these variations makes the underlying pattern easier to internalize.
Large e-commerce marketplaces
Major online marketplaces that support many independent third-party sellers face an especially acute version of this problem, since sellers themselves choose from a wide range of carriers, meaning the platform’s tracking aggregation layer must support dozens of carrier integrations simultaneously, from major international couriers down to small regional last-mile specialists used in specific countries.
Dedicated shipment-tracking platforms
A category of companies exists specifically to solve this problem as a service, offering a single API that abstracts over hundreds of carriers worldwide, so that a smaller retailer can integrate once against a unified API rather than building and maintaining dozens of carrier-specific adapters themselves. These platforms are, architecturally, a direct real-world instance of the adapter-and-normalization pattern described throughout this guide, just offered as a product rather than built in-house.
Food and grocery delivery
On-demand food and grocery delivery platforms track a much shorter, faster-moving journey, typically measured in minutes rather than days, but rely on the same core architectural ideas — a live status feed from delivery partners normalized into a canonical set of statuses like preparing, picked up, and delivered, pushed to the customer through a similar cache-backed, event-driven pipeline, just tuned for a far shorter time horizon and a much higher relative update frequency per shipment.
International freight and customs tracking
Cross-border freight tracking adds an extra layer of complexity on top of everything described so far: customs clearance events, often reported by government systems rather than the carrier itself, must be normalized into the same canonical model alongside ordinary carrier scans, and estimated delivery dates have to account for customs dwell time, which varies enormously by country, product category, and season, making the ETA Service’s underlying model meaningfully more complex than a purely domestic shipment would require.
Returns and reverse logistics
The same architecture, run in reverse, tracks a returned item’s journey back from the customer to a warehouse or processing center. This is architecturally almost identical to the forward-shipment case — the same canonical status model, the same webhook-and-polling ingestion pattern, and the same normalization logic — but it typically feeds a different set of downstream consumers, such as a refund-processing system that needs to know the moment a return is confirmed received, rather than a customer-facing notification system, since customers usually check on returns far less obsessively than they check on an anticipated delivery.
“How would you extend this design to support international shipments that pass through customs?” A solid answer treats a customs clearance event as just another entry in the canonical status model, sourced from a specialized customs-data adapter rather than the carrier adapter, and notes that the ETA Service needs an additional input specifically for expected customs dwell time by country and category, since that single factor often dominates delivery-time variance for cross-border shipments far more than the physical transit time itself.
Frequently Asked Questions
A curated set of questions that come up repeatedly in system-design interviews and in real production discussions, answered directly.
Why not just poll every carrier constantly instead of building a webhook system at all?
Constant polling wastes carrier API quota on shipments that have not changed, adds unnecessary latency compared to a push-based update, and risks hitting rate limits that could get the retailer’s API access temporarily suspended. Webhooks, where supported, give near-real-time updates far more efficiently; polling remains essential only as a backstop and for carriers without webhook support.
What happens if a carrier changes their status codes without warning?
This does happen in practice, and it is exactly why the status-mapping table lives in configuration rather than hardcoded logic, and why an “unmapped status code” should never be silently dropped. A well-designed Normalization Service logs and alerts on any raw status code it does not recognize, falling back to a safe default canonical status such as IN_TRANSIT rather than failing the event outright, while an engineer investigates and adds the new mapping.
How do you handle a shipment that switches carriers mid-journey?
This is the multi-leg case introduced earlier: the system models a shipment as one or more legs, each with its own carrier code and tracking number, and the customer-facing timeline stitches these legs together in sequence into one continuous story, rather than showing two disconnected, confusing tracking numbers.
Can a small business build a simplified version of this system?
Yes. A retailer shipping through just one or two carriers can start with a much simpler design — direct polling on a fixed schedule against each carrier’s API, with results stored in a single database and a basic cache in front of a simple status endpoint. The full adapter-based, event-driven architecture in this guide becomes necessary primarily once carrier count, shipment volume, and the need for near-real-time updates grow enough that a simple polling loop can no longer keep up.
How is this different from a general-purpose webhook processing system?
The webhook ingestion mechanics are broadly similar to any webhook-consuming system, but the domain-specific parts — the canonical shipment status model, timestamp-based ordering across multiple independent external sources, and carrier-specific rate-limited polling as a backstop — are what make this a distinct problem worth its own dedicated architecture, rather than a generic webhook pipeline with a thin layer on top.
Should the estimated delivery date be shown as a single date or a range?
Most production systems show a range, or a single date with an implicit understanding that it is an estimate rather than a guarantee, precisely because transit times have real variance even along the same lane and carrier. Showing an overly precise single timestamp risks setting an expectation the system cannot reliably meet, while a well-communicated range manages customer expectations honestly and tends to produce fewer support escalations when a shipment arrives a day later than the tightest possible estimate would have implied.
What should happen if a carrier never updates a shipment’s status for an unusually long time?
A shipment that has not received any new scan event for longer than a configured threshold for its current status is typically flagged internally as “stalled,” which can trigger a proactive customer notification acknowledging the delay, an automatic support ticket for investigation, and increased polling frequency to catch the next update as early as possible, rather than silently leaving the customer looking at an unchanged, aging status with no indication anything is being done about it.
Summary & Key Takeaways
Pulling the whole guide together in a form suitable for review, revision before an interview, or a quick refresher when the design comes up again months later.
Key takeaways
- Shipment tracking aggregation systems unify inconsistent, multi-carrier data into one canonical status model that customers can trust regardless of which carrier is actually handling their package.
- The architecture splits into an ingestion path, combining webhooks and rate-limited polling behind per-carrier adapters, and a fast, cache-first read path completely insulated from carrier availability and latency.
- An API Gateway and Load Balancer sit at the front door for customer and internal-tool traffic, handling authentication, rate limiting, and distribution before requests ever reach the Tracking Service.
- Ordering and deduplication logic, driven by each carrier’s own reported scan timestamp, is what prevents a shipment’s visible status from ever jumping backward due to duplicate or out-of-order events.
- Kafka and a stream processor connect webhook and polling ingestion into the normalization pipeline asynchronously, keeping the customer-facing read path fast and fully decoupled from slow or unreliable external carrier systems.
- Reliability patterns — circuit breakers per carrier, bulkhead isolation, multi-zone redundancy, and durable webhook queuing — matter enormously, since this system’s dependability is only as good as many external systems it does not control.
- Security deserves specific attention around webhook signature verification, given the Webhook Gateway is, by necessity, a publicly reachable endpoint.
- The same underlying architecture, with different adapters and canonical status models, generalizes to food delivery tracking, freight and customs tracking, and any domain that aggregates real-time status from many independent external partners.
Treat this system as a translation layer: many noisy, independent, external voices flowing into one calm, consistent, trustworthy story shown to a customer. Every architectural decision in this guide — from the adapter interface to the canonical status model to the cache-first read path — ultimately serves that one job. Get that mental model right, and the rest of the design falls out almost mechanically.
Where to go from here
If you are building a system like this for the first time, start small. Integrate one carrier, ship a simple polling loop into a database, put a thin cache in front, and expose a single tracking endpoint. You will get most of the observable customer benefit in a few weeks, and you will earn the operational context needed to layer on the adapter abstraction, event-driven ingestion, and reliability patterns described in this guide as your carrier count, shipment volume, and traffic actually justify each next step. This incremental path is how nearly every large tracking platform referenced in the industry examples above actually evolved, starting from a modest, largely manual process and layering in automation, per-carrier isolation, and multi-region high availability only as scale and data made the investment worthwhile.