A delivery route planner on AWS for a few dollars a month
Every morning a delivery or service round starts with the same unglamorous puzzle: here are today’s stops, here’s the van, what order do we do them in? Most small teams solve it with a driver’s instinct and a paper list, which is fine until there are thirty stops, three of them with fixed time windows, and one customer ringing to ask where the van is. This post walks through the design of a small planner that turns the day’s stops into an optimised route, hands the driver a clean manifest, and tells each customer when to expect the van — without ever taking the wheel.
Key takeaways
- Each morning the planner gathers the day’s stops, geocodes the addresses, and works out a sensible order.
- The optimiser is plain code — a distance matrix, a nearest-neighbour seed, and 2-opt — with no model on the route.
- Time windows and the van’s capacity are hard constraints: a stop that won’t fit either is never forced in.
- The driver gets an ordered manifest with access notes; each customer gets an ETA window that updates as stops are done.
- Designed on AWS for about $3.10/month at roughly 40 stops a day. The route is a proposal — the driver stays in charge.
The whole system on one page
Before any code, here’s the shape of what we’re designing. A delivery or service business starts every day with the same puzzle — a list of stops, one or two vans, and a finite number of daylight hours — and solves it the same way: someone eyeballs the addresses and picks an order that feels about right. That works at five stops and falls apart at thirty, especially once a few of them have to happen inside a window. The system below does that ordering properly, the moment the day’s list is settled, and tells everyone — driver and customers — what to expect.
What you set up once (the outside)
- The day’s stops. A Google Sheet in a Drive folder (or your existing orders system) with one row per stop: a stop reference, the customer name, the delivery address, a phone or email for the ETA, an optional time window (“before noon”, “2–4pm”), the size or weight — or for a service round, the minutes the job takes — and a free-text access note (“side gate, dog”). You already produce most of this in the normal course of taking orders; this just keeps it where the planner can read it each morning. Part 2 covers exactly how it’s gathered.
- Depot and vehicles. One short settings doc in the same folder. It holds the depot address every route starts and ends at, the vans and what each can carry (boxes, weight, or pallet count), the working day (“08:00 to 17:00”), the average time a driver spends at a stop, and which geocoding and distance-matrix provider to use. Changing the cut-off time or adding a second van is an edit here — not a deploy.
- Drivers and customers. The driver opens a link on their phone and sees the manifest — the stops in order, each with its address, window, and access note. The customer gets a single message: “Your delivery is due between 10:30 and 11:30 today,” updated if the round runs ahead or behind. Neither side ever sees the optimiser’s internals; they see a list and a window.
What runs every morning (the inside)
- Gather. A scheduled job wakes at a set time, reads the day’s stops from the mirror, and turns each address into a latitude and longitude. Addresses repeat — the same streets come up week after week — so a cache means most lookups cost nothing. It also validates each row: a stop with no usable address, an impossible window, or a load bigger than any van is pulled out and flagged rather than quietly breaking the route. This is Part 2.
- Optimise. With clean coordinates in hand, plain Python builds a matrix of travel times between the depot and every stop, seeds a route by nearest-neighbour (start at the depot, always go to the closest stop not yet visited), then runs 2-opt to uncross any legs that double back. Time windows and van capacity are checked on every candidate route; anything that would make the van late or overloaded is rejected outright. No model touches this. This is Part 3.
- Manifest. The ordered route is rendered into a driver manifest — stop 1, stop 2, stop 3, each with the address, the window, and the access note — written once to a PDF and dropped in S3, then sent to the driver as a link. The driver can re-order or skip; the manifest is the suggested order, not a locked instruction. This is Part 4.
- ETAs. As the round is planned, each customer gets an arrival window for their stop, the wording drafted by one small Bedrock call. As the driver checks stops off, those events flow back in and the remaining windows are recomputed from where the van actually is, so a slow morning slides everyone’s ETA without anyone retyping a thing. This is Part 5.
In plain words
At 6am a florist’s sheet has 34 deliveries for the day, six of them flagged “before noon” for offices that lock up at lunch. The planner geocodes the addresses — 31 are already in the cache from previous weeks, so only three are looked up — and orders the round so the six timed stops fall in the morning, the van loops out and back without crossing its own path, and the heaviest crates come off first. The driver opens the manifest at 7:40: 34 stops, in order, each with the postcode, the window, and notes like “reception, ask for Priya.” Every customer has already had a text: “Your flowers are due between 9:15 and 10:15 today.” When the driver hits traffic and falls twenty minutes behind by stop 12, the afternoon windows quietly shift and the next few customers get an updated text — no one rings to ask.
The cost of running this is about $3.10 a month at that volume. The cost of not running it is the doubled-back miles nobody measures, the “before noon” stop reached at 12:20, and the steady trickle of “where’s my delivery?” calls that land on whoever is nearest the phone.
Design rules that shaped every decision
- The plan is a proposal. The manifest is the suggested order; the driver can re-order, skip, or insert a stop at any time.
- Plain code optimises. The matrix, nearest-neighbour, and 2-opt are deterministic Python — no model decides the route.
- Windows and capacity have a veto. A route that arrives late or overloads a van is rejected, never “optimised” into existence.
- The model only writes words. Bedrock drafts the customer ETA message and nothing else; swap it for a template and lose nothing structural.
- Settings live in a doc. Depot, vans, hours, and the average stop time change without a deploy.
- Pay per morning, not per hour. Nothing runs when there are no stops to plan, so the idle bill is zero.
Why this shape
Most small rounds are planned one of three ways: the driver orders the stops from memory, someone senior spends twenty minutes with a map before the vans roll, or there’s an expensive fleet product that assumes a depot of fifty vehicles and bills accordingly. Memory is fine until the new driver doesn’t know the shortcuts. The twenty-minute map is the first thing dropped on a busy morning, and it can’t react when stop 12 runs late. And the fleet product solves a problem a two-van florist doesn’t have, at a price that doesn’t fit.
The shape above keeps the orders sheet you already maintain as the list of stops, leans on a hosted matrix service (or a simple distance estimate) for the travel times, and adds a small system that does the ordering properly every morning for the cost of a coffee. The optimisation that people assume needs AI is the boring, well-understood part — nearest-neighbour and 2-opt have been ordering routes for decades — so there’s no model on the path that matters, and the one place a model does appear, the customer text, can be turned off without breaking anything.
The next four posts walk through each piece in turn: how the day’s stops get gathered, how a route gets optimised, how a driver manifest gets built, and how customer ETAs get sent. One diagram per post. A cost breakdown and a final engineering reference at the end.
All posts