•CASE STUDY

Multi-Carrier Parcel Shipping System

4 min read·778 words·Intermediate

Asked at

1 candidate report in Mar 2026

How to use this case study

SDE-2 / Mid

  • Design shipment creation
  • Choosing a carrier (UPS, USPS, FedEx)
  • Buying a label through the carrier's API
  • Tracking status

SDE-3 / Senior

  • Go deeper on the carrier adapter abstraction
  • Rate shopping
  • Idempotent label purchase
  • Webhook + polling tracking
  • The shipment state machine

Staff / Principal

  • Discuss carrier outages and fallback
  • Rate limits
  • SLA-based routing
  • Returns
  • Reconciliation of carrier invoices

Problem RestatementProblem

Walmart asked: design a system for shipping customer parcels that supports multiple carriers (UPS, USPS, FedEx...). For each order, it should:

  • create a shipment (addresses, package size and weight, service level),
  • choose a carrier and service (cheapest that meets the delivery promise),
  • buy a shipping label through that carrier's API,
  • track the parcel until delivery and notify the customer,
  • handle carrier failures, returns and exceptions.

ArchitectureArchitecture

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    OMS["Order system / warehouse"] --> SS["Shipment Service"]
    SS --> DB[("Shipments DB")]
    SS --> RATE["Rate shopping"]
    RATE --> AD["Carrier adapters"]
    AD --> UPS["UPS API"]
    AD --> USPS["USPS API"]
    AD --> FDX["FedEx API"]
    SS -->|"buy label"| AD
    UPS -->|"tracking webhooks"| TRK["Tracking ingestion"]
    FDX --> TRK
    POLL["Tracking poller"] --> AD
    TRK --> SS
    SS --> K[("Shipment events")]
    K --> NOTIF["Customer notifications"]

Deep Dive — Supporting four carriers without four codebasesDeep dive

UPS, USPS and FedEx each have their own API, authentication, address format and status vocabulary. How that difference is absorbed decides what adding the fifth carrier costs.

Weak

Branch on the carrier in the shipping service

if carrier == 'ups': ... elif carrier == 'fedex': ... wherever a carrier is involved.
Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  SVC["Shipping service"] --> R["Rates: if/elif per carrier"]
  SVC --> L["Labels: if/elif per carrier"]
  SVC --> T["Tracking: if/elif per carrier"]
  SVC --> V["Voids: if/elif per carrier"]
  NEW["Add a carrier"] --> EDIT["Edit every branch - and miss one"]
  EDIT --> BUG["Carrier works for labels, silently unsupported for voids"]

The carrier's shape leaks into every part of the system, so each new carrier is a change in many places and a partial integration is easy to ship without noticing.

Good

A client class per carrier

Give each carrier its own class wrapping its API. Vendor details are at least contained.

Better, and the classes still have different method names and return different shapes, so the calling code branches on which client it holds. Nothing forces a new carrier to implement everything, and the shipping service still knows that FedEx returns deliveryEstimate while UPS returns estimatedDelivery.

Best

One interface, plus a normalised vocabulary

Define the contract the system needs and write an adapter per carrier against it:

interface CarrierAdapter {
  getRates(shipmentRequest)                              -> [{service, price, estimatedDeliveryDate}]
  createLabel(shipmentRequest, service, idempotencyKey)  -> {trackingNumber, labelPdfUrl, cost}
  voidLabel(trackingNumber)
  getTracking(trackingNumber)                            -> [normalised events]
}
Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  SVC["Shipping service - knows only the interface"] --> A1["UPS adapter"]
  SVC --> A2["USPS adapter"]
  SVC --> A3["FedEx adapter"]
  A1 --> N["Normalise statuses"]
  A2 --> N
  A3 --> N
  N --> V["LABEL_CREATED, PICKED_UP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION"]
  ADD["New carrier"] --> ONE["One new adapter - nothing else changes"]
  • Adding a carrier is one file. The interface also makes an incomplete integration a compile-time or startup failure rather than a runtime surprise.
  • Normalise the status vocabulary at the edge. Carriers report dozens of idiosyncratic codes; mapping them into one small set means the order page, notifications and analytics are written once. Keep the raw code alongside for support, but never let it into business logic.
  • The idempotency key belongs in the interface. Label creation costs money, so every adapter must accept one and pass it through — making it part of the contract is what stops a carrier being integrated without it.

Rate shopping falls out for free: ask every adapter for rates in parallel with a deadline, and pick by price, speed or a business rule. Because the return shape is uniform, that comparison is a sort rather than a special case per carrier.

Key FlowsFlows

3.1 Create shipment and buy label

  1. Receive a shipment request with an idempotency key (the order + package ID).
  2. Rate shopping: ask the eligible carriers for rates in parallel (with timeouts), filter the options that meet the promised delivery date, and pick by cost, reliability score and business rules (carrier allocation contracts).
  3. Buy the label with an idempotency key, so a retry after a timeout doesn't buy two labels. If the carrier's API doesn't support idempotency keys, first check whether a label for this reference already exists before retrying.
  4. Save the tracking number and label, and set the state to LABEL_CREATED. The warehouse prints the label.

3.2 Tracking

  • Webhooks from carriers that support them (verified signatures) → normalize → update the shipment.
  • Polling for the others (and as a safety net): poll active shipments, more often when "out for delivery" and less when "in transit" for days, within each carrier's rate limits.
  • The state machine only moves forward (ignore older or out-of-order events by event time), with EXCEPTION for problems (address issue, damaged).
  • Events → customer notifications ("Out for delivery today").

Failures and Operations

  • Carrier API down: circuit breaker on that adapter, and route new shipments to the next-best carrier. Queue label purchases with retries if all are down.
  • Rate limits: per-carrier token buckets for rates, labels and tracking calls.
  • Stuck shipments: no update for N days → an alert and a claim workflow.
  • Returns: create a return label (reversed addresses), and track it the same way.
  • Invoice reconciliation: compare carrier invoices (actual weight and dimensions surcharges) with quoted costs, and flag differences.

Wrap-UpWrap-up

Hide each carrier behind an adapter implementing get rates, create or void label and get tracking, with normalized statuses. Shop rates in parallel and pick the cheapest option that meets the promise, buy labels idempotently, and track parcels through verified webhooks plus rate-limited adaptive polling into a forward-only shipment state machine that drives notifications. Protect against carrier outages with circuit breakers and fallback carriers, and reconcile carrier invoices afterwards.

More Case Studies

Frequently Asked Questions

What is the Multi-Carrier Parcel Shipping System system design question?

Multi-Carrier Parcel Shipping System is a system design interview question asked at FAANG companies. It covers e-commerce, api design, event driven, distributed systems and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Multi-Carrier Parcel Shipping System question?

Walmart have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Multi-Carrier Parcel Shipping System interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Multi-Carrier Parcel Shipping System question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →