Build Amazon's Warehouse Picking Route Optimizer


scalability distributed-systems performance

System Design Deep Dive

Warehouse Picking Route Optimizer

Batch orders, solve a walking-distance TSP across a million square feet, and re-route a picker mid-aisle without ever handing them a worse path.

⏱ 14 min read 📐 Advanced 🏭 Scalability

Picture a grocery shopper handed a list of forty items with no thought given to where anything sits on the shelves. They will find all forty eventually, but they will crisscross the same three aisles five or six times doing it, because the list was written in the order the items were requested, not the order they physically appear in the store. Now stretch that supermarket into a fulfillment center the size of eighteen football fields, hand the list to four hundred different shoppers at once, and scatter each list’s items across zones that can be a quarter mile apart. The cost of a badly ordered list stops being a minor inconvenience and starts being the difference between a warehouse that ships every promised order on time and one that quietly falls behind every single shift.

The hard part is not finding a path between two shelves. Any engineer can call a pathfinding library and get a walking distance between two points in a warehouse. The hard part is doing that for a facility with well over a hundred thousand distinct bin locations, deciding which orders should even travel together in the same trip before you route anything, computing a near-optimal visiting order for a dozen-plus stops in a few hundred milliseconds, and then throwing that whole plan away and adjusting it on the fly the moment a rush order needs to be shoved into a walk that is already half finished. A picker standing still waiting for their next assignment is a picker who is not shipping orders, and at this scale someone’s next assignment is always about to be needed.

A naive system assigns orders to pickers one at a time, first-come-first-served, and calls the routing problem solved by handing over the item list in whatever order the orders arrived. That fails for two independent reasons. First, a single-order-per-trip picker walks the full distance between every item regardless of how close those items actually sit to each other, so the facility’s total labor cost scales linearly with order count no matter how cleverly you route any one trip. Second, even once you start bundling multiple orders into a single trip to amortize the walk, visiting a batch’s stops in the wrong sequence can turn a fifteen-stop trip that should take twelve minutes into one that takes twenty, because computing the truly shortest path through a batch of stops is the traveling salesman problem, and the traveling salesman problem does not have a fast exact solution once the stop count climbs past a dozen or so. Solve both of those and you still have to handle the case that matters most operationally: a new order needs to go out in the next truck, a picker is already three stops into a nine-stop walk, and stopping to recompute their entire route from scratch would either add unacceptable latency or discard progress they have already made.

We need to solve for three things simultaneously: representing a million square feet of aisles, cross-aisles, and pick faces as a graph that can answer “how far apart are any two points” in milliseconds, computing a near-optimal visiting order for a batch of stops without ever running an exact TSP solve, and adjusting a picker’s plan in flight when a new or priority order needs a stop added mid-walk, without throwing away the work already done.

Requirements and Constraints

Functional Requirements

  • Represent the warehouse’s aisles, cross-aisles, pick faces, staging lanes, and pack stations as a navigable graph with accurate walking distances between any two points
  • Batch incoming orders into picker-sized work units (roughly 8 to 20 items) that minimize total walking distance while respecting cart and tote capacity
  • Compute a near-optimal visiting sequence (an approximate TSP solution) for every batch, favoring zone locality over strict item-arrival order
  • Assign each computed batch to an available picker based on the picker’s current location, zone specialization, and recent utilization
  • Dynamically re-route an in-progress picker when a new order fits their current path, and support injecting a priority or rush order mid-walk without discarding their existing progress
  • Track pick progress in real time through scan confirmations and detect exceptions like short-picks (item not physically present at its expected location)
  • Support zone-based picking, where an order spanning multiple far-apart zones is split into per-zone sub-picks that converge at a consolidation or pack station

Non-Functional Requirements

  • Facility scale: 1,000,000 sq ft, roughly 40 pick zones, about 120,000 discrete bin and slot locations, modeled as a graph of roughly 18,000 aisle-intersection and pick-face nodes
  • Order throughput: sustained 15,000 orders/hour in normal operation, bursting to 50,000 orders/hour (about 14/sec) during promotional peaks
  • Active pickers: 300 to 500 concurrent pickers per shift across three shifts
  • Batch route computation latency: p99 under 300ms per batch, so a picker is never left standing idle waiting on their next assignment
  • Re-route decision latency: under 500ms from “new order becomes eligible for injection” to “updated route pushed to the picker’s handheld”
  • Graph freshness: aisle closures, temporary blockages, and slotting changes propagate into the routing graph within 60 seconds
  • Route quality: computed routes stay within 10 to 15% of true-optimal walking distance, since an exact TSP solve is infeasible inside this latency budget
  • Availability: picking operations target 99.9% uptime during operating hours; the system degrades to simple nearest-neighbor routing rather than failing closed if the optimizer is unavailable

Constraints and Assumptions

  • We assume each picker carries a single handheld or wearable scanner that displays one active route at a time; we do not design the handheld hardware or its scanning firmware
  • We assume slotting (which SKU lives in which bin) is owned by a separate inventory and slotting system; “where does SKU X live right now” is treated as a lookup, not a decision this system makes
  • We do not cover pack station logistics, outbound trailer loading, or last-mile delivery routing
  • We assume the physical graph topology (aisle layout, doors, conveyor crossings) changes infrequently relative to the order stream, on the order of a weekly slotting cycle, so a full graph rebuild can run offline while incremental edge-weight updates for temporary closures stay near-real-time

High-Level Architecture

The system has six major components: the Order Intake and Batching Service, the Warehouse Graph Service, the Route Optimization Engine, the Picker Task Dispatcher, the Order and Scan Event Stream, and the Picker Mobile Client.

Architecture overview showing the order intake and batching service, warehouse graph service, route optimization engine, picker task dispatcher, event stream, and picker mobile client

The Order Intake and Batching Service is where individual orders stop being individual: it groups incoming orders into picker-sized batches, clustering by which pick zones their items touch so a single trip does not require crossing the whole facility. The Warehouse Graph Service owns the physical model of the building: aisles, cross-aisles, pick faces, and staging areas as a graph, with a precomputed distance cache so a “how far apart are these two points” query never requires a live pathfinding search. The Route Optimization Engine takes a batch and the distance cache and produces a near-optimal walking sequence using a TSP approximation, and it is also where a priority order gets stitched into an already-in-progress picker’s remaining stops. The Picker Task Dispatcher decides which idle or soon-to-be-idle picker receives a given computed route, balancing proximity against fairness so work does not pile up on the pickers closest to the loading dock. The Order and Scan Event Stream is the backbone that carries new orders, priority flags, and scan confirmations between every other component without anyone polling. The Picker Mobile Client is the handheld or wearable that renders the turn-by-turn stop list and reports scans back into the stream.

A batch’s life looks like this: orders land in the intake service and sit in a short queue until a wave closes, either because a timer expires or the batch hits capacity. The closed batch, along with the relevant slice of the distance cache from the Warehouse Graph Service, goes to the Route Optimization Engine, which returns a sequenced stop list within its latency budget. The Picker Task Dispatcher matches that route to a picker and pushes it to their mobile client. As the picker scans items, those confirmations flow back through the event stream, and if a new order arrives that plausibly fits inside that picker’s remaining walk, the event stream triggers the Route Optimization Engine’s fast insertion path instead of a full re-solve, and the updated route lands on the same picker’s handheld before they reach their next stop.

Key Insight

The single most important architectural decision is splitting route computation into two distinct algorithms with two distinct cost profiles: an expensive "build a route from scratch" path used only when a batch first forms, and a cheap "insert one stop into an already-committed route" path used for every subsequent priority order or dynamic addition. Trying to use one algorithm for both jobs either makes new-batch computation too slow or makes every mid-walk update disruptive enough to undo the picker's progress.

Component Deep Dives

The Order Intake and Batching Wave

This component’s job is to decide which orders travel together in the same picker trip before any route is ever computed.

The obvious mistake is treating batching as an afterthought, batching orders strictly in arrival order until a cart is full. That produces batches whose items are scattered uniformly across the facility, and no amount of clever sequencing afterward can fix a batch that was doomed at formation, since a TSP solver can only optimize the order you visit stops in, not shrink how far apart those stops physically are. We batch by grouping orders whose items share zone footprint, using something close to a similarity clustering: two orders that both need items from zones 4 and 7 are strong candidates to travel together, while an order needing only zone 19 items is a weak fit for that same batch.

Orders arrive continuously, so batches form on a rolling wave: every 90 seconds, or sooner if a batch reaches its capacity limit (typically 12 to 20 stops per cart), the intake service closes whatever batch it has been assembling and hands it off. A wave timer that never closes would let pickers sit idle waiting for a “perfect” batch that never arrives during a slow period; a wave that closes too eagerly forfeits locality gains during a busy period when a slightly longer wait would have produced a much tighter batch.

# Wave-based order batching by zone-footprint similarity
# Demonstrates: greedy clustering of pending orders into capacity-bound batches
from dataclasses import dataclass, field
from time import monotonic

WAVE_INTERVAL_SECONDS = 90
MAX_BATCH_CAPACITY = 18          # stops a single cart/tote can carry
MIN_ZONE_OVERLAP = 0.35          # Jaccard similarity threshold to join a batch

@dataclass
class PendingOrder:
    order_id: str
    zones: set[str]
    item_count: int
    queued_at: float = field(default_factory=monotonic)

def zone_similarity(a: set[str], b: set[str]) -> float:
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)

def form_batches(pending_orders: list[PendingOrder]) -> list[list[PendingOrder]]:
    # Oldest orders seed batches first, so nothing waits past the wave deadline.
    remaining = sorted(pending_orders, key=lambda o: o.queued_at)
    batches: list[list[PendingOrder]] = []

    while remaining:
        seed = remaining.pop(0)
        batch = [seed]
        batch_zones = set(seed.zones)
        stops_used = 1

        candidates = sorted(
            remaining,
            key=lambda o: zone_similarity(batch_zones, o.zones),
            reverse=True,
        )
        for candidate in candidates:
            if stops_used >= MAX_BATCH_CAPACITY:
                break
            if zone_similarity(batch_zones, candidate.zones) < MIN_ZONE_OVERLAP:
                continue
            batch.append(candidate)
            batch_zones |= candidate.zones
            stops_used += 1
            remaining.remove(candidate)

        batches.append(batch)

    return batches

Think of this like a shuttle van dispatcher deciding which airport passengers ride together, grouping people whose neighborhoods overlap rather than filling seats strictly in check-in order. What breaks if you skip zone clustering: a batch of 18 orders whose items are spread evenly across all 40 zones gives the Route Optimization Engine nothing to work with, since visiting 18 stops that are all roughly equidistant from each other has no meaningfully “better” order, and the picker ends up walking close to the worst-case distance no matter how the TSP solver sequences the stops.

Premium Content

Unlock the full article along with everything else in the archive — all in one place.

In-depth analysis Expert insights Full archive access
Unlock Full Article