Building a Warehouse Picking Optimizer Under Real Constraints
How I turned ERP pick sheets into optimized warehouse routes by modeling the floor, validating heuristics against an exact solver, and shipping under tight operational constraints.
TL;DR
I built Vivaldi Warehouse Logistics, a full-stack system that turned ERP pick lists into optimized batch order routes, with inventory search, bulk updates, barcode scanner integration, and error tracking. The optimizer evolved through TypeScript, C++, and Rust/WASM as I learned that model fidelity and deployment simplicity mattered more than algorithm cleverness alone.
On this page
Overview
A warehouse operator fulfilling batch orders needs two things to work fast: locate the right items across the floor, and pick them up along the shortest route. Get both right and you cut into the single biggest cost in warehouse operations; order picking accounts for 55-60% of total operating expenses, and most of that cost is walking between picks. Get either wrong and operators zigzag across aisles, backtrack past racks they already visited, and introduce costly slowdowns across the entire picking pipeline.
At Vivaldi srl, the ERP produced pick lists in arbitrary order. No routing logic, no spatial awareness. Operators scanned a barcode, received a flat list of parts and quantities, and had to mentally construct a path through 9 aisles on their own. Small orders survived this. Batch orders spanning the full warehouse did not: redundant trips, missed turns, accumulated inefficiency compounding with every order fulfilled.
I built Vivaldi Warehouse Logistics to solve both problems at once. The primary feature was route optimization: given an order and current inventory, resolve which shelf slots to visit, compute a picking route, and return a ready-to-follow sequence within seconds of the barcode scan. But route planning alone does not make a product. The system also handled bidirectional inventory search (from a position like A1.5 to the item stored there, or from a mnemonic code to its warehouse location), individual and batch inventory updates via CSV upload, full warehouse snapshots as downloadable Excel files, first-class barcode scanner support across every workflow, in-house error tracking with automated reporting, and a code generation pipeline that propagated layout changes to both the web application and database.
This was a production system under real constraints: a shared Debian server with 4 GB of RAM, no Docker, no CI/CD, a live Microsoft SQL Server ERP I could not redesign, and one engineer. Over 2.5 years I evolved the optimizer through three implementations and learned that model fidelity and deployment simplicity mattered more than algorithm cleverness alone. This case study focuses on the route optimization problem.
Stack. React frontend, Node.js/Koa.js REST API, Rust/WASM optimizer, Microsoft SQL Server (shared with the ERP), gRPC for internal services (CSV updater, error emailer), systemd on Debian for deployment.
Reading a Position Code
Every location in the warehouse has a compact code like E5.2. Understanding this convention makes the rest of the case study immediately legible.
Everything before the dot is the floor position: aisle E, rack 5. This tells the operator where to walk. The number after the dot is the shelf position: which tier to pick from. Separating these two concerns let me optimize for both walking distance and ergonomics.
Codes without a dot suffix (like E5) refer to the rack as a whole. Most racks had 6 shelf tiers; larger racks in aisles H and I had up to 12.
The Warehouse Floor
Vivaldi’s warehouse had 9 main aisles labeled A through I, plus a smaller section J. Each aisle contained up to 12 racks, and each rack held multiple shelf tiers. Aisles B through I formed repeated rack blocks separated by corridors. Aisle A ran along the left wall. Section J occupied a separate corner.
The layout was regular enough to model as a grid, but not so regular that I could treat it as a toy problem. The interactive map below renders the actual grid the optimizer works on. Now that you know how to read position codes, the labels on the map should make sense. Click any rack to inspect its shelf slots, or press “Play Route” to watch a real batch order animate through its optimized pick sequence.
Route manifest · 8 picks
- 00StartEntrance
- 01A2.5pick
- 02B3.2pick
- 03A5.2pick
- 04C7.3pick
- 05E5.1pick
- 06I5.4pick
- 07H1.3pick
- 08H2.6pick
- 09ReturnEntrance
From Order to Route
Operators walked the warehouse pushing a cart, collecting items along a tour that started and ended at the depot.
Multiple times a day, an operator scanned an order barcode. The system loaded the requested parts and quantities from the ERP, then looked up every slot where each item was available, including current stock levels. A single item could exist in multiple positions, so the core problem was: given a set of requested items with known storage locations, group and sequence the picks so that the picker’s tour is minimized.
The client added two constraints on top of pure distance. First, favor ground-level shelf tiers to reduce reaching and bending. Second, prefer draining nearly-empty slots before touching fuller ones. If an order requests 10 copies of item XYZ and it is available at A1.1 (8 in stock), B2.1 (50 in stock), and C3.1 (25 in stock), always pick from A1.1 first to exhaust it, then choose between B2.1 and C3.1 based on whichever fits the route better. This kept inventory consolidated instead of leaving single-digit remnants scattered across the warehouse.
What the business needed was not an optimization score. It was a pick list an operator could follow on the spot.
Without optimization, a batch order might send the operator to C6, then across the warehouse to I5, then back to H2, then west again for what was missed. An optimized route starts near the entrance and sweeps through adjacent aisles: A2, B3, A5, C7, E5, I5, H1, H2. That route had to appear within seconds of the barcode scan, or it would break the operator’s workflow.
Turning the Floor Into Data
The first useful step was not a production service. It was a crude model I could see and test.
I started with a hardcoded grid where each cell was either walkable corridor or shelf rack. Distances were uniform: every step cost the same. I built this in a Python notebook, plotted the grid, and ran sample orders through a greedy nearest-neighbor by hand. Rough, but it made the optimization problem concrete enough to reason about.
To make the layout editable without touching code, I described rack runs in a compact format:
[ A1:6, 0, 0, B1:6 ]A1:6 means aisle A, racks 1 through 6. 0 means walkable corridor. Physical dimensions came later:
rack_bay_width_m = 2.7corridor_width_m = 1.2Three layers, each building on the last. Slot labels made the warehouse legible to operators. The floor grid made it computable. Physical dimensions made the distances faithful to how people actually walk.
The Exact Formulation: A Reference, Not a Product
Before building the production heuristic, I wrote the problem as an exact mathematical formulation: given requested items across multiple slots, choose which slots to visit, how many units to pick from each, and in what order, minimizing total walking distance.
This was never going to run in production. Exact solvers scale poorly, and even modest orders blew past any reasonable response budget. I wrote it for a different reason: it gave me a correctness oracle. On small cases (5-6 items, a few aisles), I could compute the provably optimal route and compare it against whatever heuristic I was testing. If the heuristic diverged wildly on cases the exact solver could handle, something was wrong with my approach.
I did not have a formal benchmark suite during the original project. I reasoned in terms of recurring scenarios and failure cases: orders spanning every aisle, orders clustered in one corner, orders with the same item in multiple distant slots. The formal measurement story came later.
Making It Production-Feasible
TypeScript: Correct but Too Slow
The first solver was pure TypeScript. It produced correct results on small orders, but large batches with 10+ items blocked the Node.js event loop for seconds. HTTP requests timed out. Operators waited. The algorithm was sound; the runtime was not.
C++ via N-API: Fast but Fragile
I rewrote the solver in C++ using simulated annealing, bound to Node.js via N-API. Route optimization dropped from seconds to milliseconds.
The tradeoff was deployment. The server ran Debian; macOS binaries would not run. I needed a C++ toolchain on the production server for one library. Stack traces from the FFI boundary were useless. Every deployment felt fragile. I lived with it for two years because the solver worked and business priorities shifted elsewhere.
Rust + WASM: Portable and Deterministic
WebAssembly eliminated every deployment problem. No native toolchain, no cross-compilation, no brittle binaries. WASM runs anywhere Node.js runs.
The rewrite also changed the algorithm. Simulated annealing was overkill for fewer than 100 positions; it spent compute exploring random perturbations when the problem space was small enough for a deterministic, multi-phase greedy approach that could encode the client’s constraints directly. The solver ran four phases in sequence, each building on the previous output:
- Availability filtering. Cap each item’s request to available stock. Flag shortfalls so the operator knows immediately what cannot be fulfilled.
- Depletion-aware slot selection. For each item, prefer the slot with the fewest units in stock. If 10 copies of
XYZare needed andA1.1has 8, pick those 8 first; then take the remaining 2 from whichever fuller slot fits the route better. This drained nearly-empty positions instead of leaving scattered remnants. - Ergonomic tier ordering. Ground-level shelf tiers first (tiers 1, 4, 7, 10), then remaining picks sorted by distance from the last ground-level stop.
- Route sequencing. Nearest-neighbor traversal starting from the depot, with a 2-opt local improvement pass to eliminate obvious crossing paths.
Stochastic optimization cannot naturally express priorities like “drain this slot first” or “prefer ground-level picks”; a phased greedy can encode them as explicit stages.
The binary compiled to ~144 KB, produced deterministic output (same input, same route, every time), and had a type-checked boundary: tsify auto-generated TypeScript types from Rust structs.
Precomputation Was the Real Win
Raw speed mattered less than avoiding repeated work. With ~96 named positions, the pairwise distance cache held about 4,560 entries. A symmetric key type halved the pathfinding computations (distance from A to B equals distance from B to A). After a brief warm-up, nearly all lookups were cache hits.
A build.rs script read the warehouse grid at compile time and generated a perfect hash map for O(1) position lookups, a static grid literal, and a coordinate lookup function. A layout change required three steps: edit the grid file, run the SQL generation script, recompile. Malformed grids failed the build.
From Uniform to Weighted Distances
The uniform-distance model was useful for getting the system running, but it lied about real walking effort. Moving between racks in the same aisle takes fewer steps than crossing a wide corridor, and the model treated both as equal.
Introducing physical dimensions into the distance computation made route estimates more faithful to how operators actually walk. This is where the story becomes about more than algorithm tuning: improving the model itself produced better routes than switching to a fancier solver ever could. Model fidelity mattered as much as algorithm choice.
The Rest of the System
Route optimization was the core, but operators needed more than a route planner.
Inventory snapshot. A full warehouse export (dump.xlsx) gave the team an offline snapshot for auditing. Import from a canonical 4-column spreadsheet could bulk-update the warehouse after a reorganization, running in a single atomic transaction so a bad file could not leave the database half-updated.
Item management. Search any item by stock ID to see every slot where it is stored, or update quantities after a physical count.
Error monitoring. System errors forwarded to administrators via batched email. A log transport piped from stdout, filtered by severity, and sent via gRPC to a separate emailer service. If the emailer crashed, the main API kept serving.
These features were not exciting, but without them the system would have been a solver demo, not a working product.
Results
The system met its primary constraint: route computation completed fast enough to stay within the operator’s scanning workflow. Operators scanned a barcode and received an optimized pick list before they had time to start walking.
I should be honest about what I do not have. The API logged execution time per request, but those measurements were not persisted anywhere queryable. I cannot produce p95 latency figures or cache hit rates. I know the system was fast enough because operators used it daily without complaint, but “fast enough” is not a number. If I were building this again, I would add structured metrics from day one.
The Local-First Rewrite
After the engagement ended, I rebuilt the project independently on a different stack. My own time, my own initiative, no client involvement. I wanted to revisit three things: modeling fidelity, offline-first behavior, and benchmarkability.
The local-first version turned the scenario classes I had carried in my head during the original project into a structured benchmark suite. I defined scenarios based on the patterns that mattered: orders spanning all aisles, orders clustered in one section, orders with multiple candidate slots per item. I used the exact reference solver to measure how heuristic revisions performed against provably optimal routes on small, exact-solvable cases.
This is the project where I finally formalized the measurement story. The benchmark suite is retrospective, but the scenarios are faithful to the tradeoffs the original system handled. Having an exact reference changed how I iterated: instead of guessing whether a change helped, I could measure the gap against optimal and decide with confidence.
What This Project Shows
I modeled a physical warehouse from first principles, starting with a crude grid and iterating toward a model that captured real walking effort. I used an exact mathematical formulation as a reference tool because it gave me a way to validate heuristic decisions. I shipped a production system under real constraints: limited memory, no Docker, no CI/CD, a single engineer, and an existing ERP I could not redesign.
Each of the three implementations taught me something different. TypeScript showed where latency hides. C++ showed that deployment matters as much as performance. Rust/WASM showed that the right packaging eliminates entire categories of operational pain.
The system was more than a solver. It included inventory management, bulk import/export, error monitoring, and an operator-facing UI. When the engagement ended, I rebuilt the project on my own terms and added the benchmarking infrastructure I wished I had from the start.