Designing a Same-Day Delivery Routing System

Designing a Same-Day Delivery Routing System

Designing a Same-Day Delivery Routing System

How to dynamically route orders to the nearest fulfillment center that has both the inventory and the delivery capacity to get a package to a customer’s door before the sun goes down.

01

Introduction & History

Imagine you order a phone charger at 11 in the morning and it is sitting on your doorstep by 6 in the evening the same day. To you, this feels almost like magic. Behind the scenes, however, a large distributed system just made dozens of split-second decisions: which warehouse has the charger in stock, which warehouse is close enough to reach you in time, which warehouse has a delivery van and a driver free this afternoon, and how to reserve all of that before someone else buys the last unit.

This is the same-day delivery routing problem, and it sits at the intersection of e-commerce, logistics, and distributed systems engineering. It is one of the hardest operational problems in retail technology because it must be solved correctly, in under a few hundred milliseconds, thousands of times per second, across a network of physical locations that are constantly changing state: stock levels drop, vans fill up, roads get congested, and staff go on break.

Same-day delivery is not a new idea in concept. Local shopkeepers have always known their own inventory and could promise same-day service simply because they were the only option. What changed is scale. Once a company operates hundreds of warehouses, dark stores, and micro-fulfillment centers spread across a country, the decision of “who serves this order” stops being obvious and becomes a genuine computer science problem: a real-time optimization problem under constraints of distance, stock, capacity, and cost.

The commercial pressure to solve this well is enormous. Amazon popularized fast delivery expectations with Prime, and competitors such as Walmart, Target, Flipkart, BigBasket, Blinkit, Instacart, and DoorDash all built their own variants of the same underlying system: a routing engine that decides, for every order, which physical location should fulfill it. What started as “ship from the nearest warehouse” has evolved into “ship from the warehouse that is near enough, has the item, has a delivery slot, and costs the least to serve” — a multi-dimensional optimization performed live, order by order.

1.1 How we got here

It is worth tracing how we got here, because the history explains a lot of the design choices we will make later. In the early days of e-commerce, a company might operate a single central warehouse. Every order, no matter where the customer lived, was picked and shipped from that one building, and delivery took days simply because of physical distance. As order volumes grew, companies began opening a second warehouse, then a third, spreading them across different cities and regions. This solved the raw capacity problem (more people packing boxes) but introduced an entirely new question that did not exist before: given several warehouses, which one should serve a given order?

Era 1

Single Central Warehouse

Every order shipped from one building regardless of geography; delivery took days simply because of physical distance and there was no routing decision to make.

Era 2

Static Rule-Based Routing

Multiple warehouses appeared, but assignment was governed by fixed rules like “ship from the warehouse in the customer’s home state.” Works only while every warehouse stocks every product and never runs out of capacity.

Era 3

Dynamic Real-Time Routing

An online engine evaluates the actual, current state of the network for every single order, combining distance, stock, capacity, and cost inside the checkout flow itself — the design this tutorial is about.

At first, the “which warehouse” question was answered with simple, static rules: “always ship from the warehouse in the customer’s home state,” for example. This works fine as long as every warehouse stocks every product and has unlimited capacity, but neither assumption survives contact with reality. Warehouses specialize (a large item might only be stocked in a handful of locations), demand is uneven (one city’s warehouse might be swamped on a sale day while another sits half idle), and delivery fleets are not infinite. Static, rule-based routing eventually breaks down and has to be replaced with dynamic, real-time routing — a system that looks at the actual, current state of the network for every single order, rather than relying on a fixed rule written months earlier.

The next big shift was the compression of delivery time itself, from “a few days” down to “same day” and, in some markets, “within the hour.” This compression is what turns routing from a nightly batch job (deciding overnight which warehouse ships which order the next morning) into a real-time, online decision that must be made inside the customer’s checkout flow, in milliseconds, using data that might be seconds old. That shift — from batch to real-time — is the central engineering challenge this tutorial is about, and it is why the system we are about to design looks the way it does: fast candidate filtering, strongly consistent reservations, and heavy use of asynchronous processing for everything that is not strictly required to happen before the customer sees a confirmation.

Real-life analogy

Think of the whole system like a citywide network of pizza shops for one pizza brand. When you order online, the brand’s system must pick the shop that (a) has your toppings in stock, (b) has an oven and driver free right now, and (c) can reach your house while the pizza is still hot. Same-day delivery routing is this exact problem, just generalized to any product and a much longer time window than “still hot.”

In this tutorial, we will design this system from first principles. We will assume you have no prior background in distributed systems, and by the end you will understand not just what the system looks like, but why each piece exists, what breaks if you remove it, and how real companies operate systems like this at scale.

02

Problem & Motivation

Let’s state the problem precisely, because a system design interview or a real engineering effort lives or dies on a precise problem statement.

2.1 The problem, stated

Given: a customer places an order containing one or more items, along with a delivery address, at a specific point in time. The company operates a network of fulfillment centers (warehouses, dark stores, or micro-fulfillment hubs), each of which independently tracks its own inventory and its own delivery capacity for the day (number of drivers, vehicle slots, and delivery time windows remaining).

Goal: select one fulfillment center (or, if necessary, a small set of centers) that can supply every item in the order and deliver it to the customer’s address before the same-day cutoff, while minimizing cost (distance travelled, number of centers used) and maximizing the likelihood that the promise is actually kept.

2.2 Why it’s hard

This sounds simple until you consider the constraints that make it hard in practice:

  • Inventory is a moving target. Stock levels change every second as other customers buy the same items. A center that had 3 units in stock when you loaded the product page might have 0 by the time you click “Buy.”
  • Capacity is a shared, perishable resource. A delivery slot for a driver at 3 PM today is only useful if it is booked before 3 PM today. Unlike inventory, unused capacity cannot be stored for tomorrow — it simply evaporates.
  • Multiple items may live in different centers. If a customer orders a phone case from Center A and a phone charger only available at Center B, the system must decide: split the order into two shipments, or fall back to a center that has both, even if it is farther away?
  • Correctness under concurrency. Thousands of customers may be ordering the last unit of a popular item at the same instant. The system must never oversell — two customers cannot both be promised the same physical unit.
  • Latency budget is tiny. The routing decision has to happen inside the checkout flow, which customers expect to complete in under a second or two, end to end.
  • Geography is not just straight-line distance. A center 2 kilometers away across a river with no bridge may be effectively farther than one 8 kilometers away on a highway. Real routing needs road network distance or drive-time estimates, not just geographic distance.

2.3 Why it matters commercially

Why does this matter commercially? Same-day delivery is now a competitive necessity in many markets, not a luxury. Every extra minute a routing decision takes translates into slower checkout and lost sales. Every wrong routing decision — promising a delivery window a fulfillment center cannot actually meet — translates into a broken promise, a refund, and a customer who trusts the platform a little less. The routing engine is therefore both a distributed systems problem and a trust-building product feature.

2.4 Subtler edge cases

There are also several subtler edge cases that a naive first design tends to miss, and calling them out early saves a lot of pain later:

  • The cutoff is a moving target. “Same day” does not mean the same fixed time everywhere; it depends on how late in the day the order was placed and how much drive time and pick-and-pack time remain. An order placed at 9 AM has a much larger set of viable centers than one placed at 4 PM.
  • Weather and traffic change effective distance. A center that is normally a comfortable 20-minute drive away may become unreachable within the promise window during heavy rain or a major traffic event, even though its geographic distance has not changed at all.
  • Partial cancellations must release resources cleanly. If a customer removes one item from an order after a center has already been reserved, the system needs to release just that item’s stock hold without disturbing the rest of the order’s reservation.
  • Fairness across fulfillment centers matters operationally. Always picking the single closest center for every nearby customer can overload that one center while a slightly farther center sits idle; a good routing engine spreads load intelligently, not just greedily.
💬
What an interviewer may ask

“Why can’t you just pick the geographically nearest warehouse every time?” A strong answer explains that nearest-by-distance ignores stock availability and delivery capacity — the nearest center might be out of stock or fully booked, so the system needs a scoring function that combines distance, inventory, and capacity, not a single-dimension nearest-neighbor lookup. A stronger answer adds that always picking the nearest center also creates load imbalance, overloading popular centers while under-utilizing others nearby.

03

Core Concepts & Terminology

Before we design the architecture, let’s build a shared vocabulary. Each term below includes a simple explanation, a real-life analogy, and where it shows up in our system.

3.1 Fulfillment Center

A fulfillment center is any physical location that can hold inventory and hand it off for delivery — a large warehouse, a smaller “dark store” (a mini-warehouse not open to walk-in customers), or a micro-fulfillment hub tucked into a city neighborhood. Think of it like a chain of local kitchens for a food delivery app: each kitchen can only cook what ingredients it has, and can only send out as many delivery riders as it currently has free.

3.2 Serviceable Area

Every fulfillment center has a serviceable area: the geographic zone it is realistically able to deliver to within the same-day promise. This is usually defined as a radius or a drive-time polygon (an irregular shape based on road networks, not a perfect circle) around the center. A customer’s address must fall inside at least one center’s serviceable area to be eligible for same-day delivery at all.

3.3 Real-Time Inventory

Unlike a traditional e-commerce catalog that might show “In Stock” as a lazy, occasionally-refreshed flag, same-day delivery requires near real-time, per-location stock counts. If Center A has 2 units of an item, the system must know that exact number, right now, not the number from five minutes ago.

3.4 Delivery Capacity

Capacity represents how many more deliveries a fulfillment center can promise today. It is a function of the number of drivers or riders on shift, the number of vehicles available, and how full existing delivery routes already are. Capacity is consumed the moment an order is assigned to a delivery window, and it resets only at the start of the next operating day.

3.5 Routing Engine

The routing engine is the brain of the system: the service responsible for taking an order’s items and delivery address, evaluating candidate fulfillment centers, and picking the best one. This is conceptually similar to how a ride-hailing app matches a rider to the best available driver, except here we are matching an order to the best available warehouse.

3.6 Reservation (Soft Hold)

When the routing engine picks a center, it does not simply trust that the stock will still be there a second later. It places a short-lived reservation — a soft hold — on the specific units and the specific delivery slot, which is confirmed once payment succeeds and released automatically if the order is abandoned.

3.7 Order Splitting

When no single center can fulfill every item in an order within the delivery window, the system may split the order into multiple shipments from multiple centers. This trades a slightly worse customer experience (two deliveries instead of one) for a much higher fulfillment success rate.

3.8 Geohashing

A geohash is a clever way of turning a latitude and longitude pair into a short string, such that nearby locations tend to share the same string prefix. For example, every address inside the same city block might share the first eight or nine characters of their geohash. This matters because it lets the Geo Index Service answer “what fulfillment centers are near this address” using simple, fast string-prefix lookups instead of comparing the address against every single center’s coordinates one by one, which would be far too slow at scale. Think of it like a postal code system that gets more precise the more digits you add — the first digit narrows you down to a huge region, and each additional digit zooms in further.

3.9 Consistent Hashing

Consistent hashing is a technique for spreading data (or requests) across a set of servers or database shards in a way that minimizes disruption when servers are added or removed. Picture a circular dial with numbers from 0 to a very large maximum. Both the data (in our case, a fulfillment center’s inventory shard) and the servers are placed at points on this dial based on a hash function, and each piece of data belongs to the next server found by moving clockwise around the dial. The benefit is that adding a new shard only reshuffles a small slice of the dial near it, rather than requiring every single piece of data in the system to be relocated, which is exactly what we want when the fulfillment network grows.

3.10 Idempotency

An operation is idempotent if performing it more than once has the same effect as performing it exactly once. This matters enormously in a distributed system where network retries are common — if a client’s confirmation request times out and it retries, the Order Service must recognize the retry (usually through a unique idempotency key attached to the original request) and avoid creating a second, duplicate reservation or a second charge for the same order.

3.11 Same-Day Cutoff

The same-day cutoff is the latest point in the day by which an order must be placed, and a fulfillment center reserved, for same-day delivery to still be realistically achievable. It is not a single fixed clock time across the whole network; it is computed per candidate center based on that center’s remaining pick-and-pack time, drive time to the customer, and remaining delivery capacity for the day.

📌
Beginner analogy

Think of the whole system like a citywide network of pizza shops for one pizza brand. When you order online, the brand’s system must pick the shop that (a) has your toppings in stock, (b) has an oven and driver free right now, and (c) can reach your house while the pizza is still hot. Same-day delivery routing is this exact problem, just generalized to any product and a much longer time window than “still hot.”

04

Architecture & Components

Now let’s assemble the pieces into a full system. Every request from a customer’s phone or browser enters through an API Gateway, which is the single, well-guarded front door for all external traffic. From there, each internal hop passes through its own load balancer before reaching a horizontally-scaled service, so that no single service instance becomes a bottleneck or a single point of failure.

flowchart TB A1[“Mobile App”] –> GW[“API Gateway Auth Rate Limit Routing”] A2[“Web App”] –> GW GW –> LB1[“Load Balancer Order Service Cluster”] LB1 –> OS[“Order Service Validates and Creates Orders”] OS –> LB2[“Load Balancer Routing Engine Cluster”] LB2 –> RE[“Routing Engine Finds Nearest Fulfillment Center”] RE –> LB3[“Load Balancer Inventory Service Cluster”] LB3 –> INV[“Inventory Service Real Time Stock Lookup”] RE –> LB4[“Load Balancer Capacity Service Cluster”] LB4 –> CAP[“Capacity Service Delivery Slot and Driver Availability”] RE –> GEO[“Geo Index Service Fulfillment Center Locator”] RE –> CACHE[“Distributed Cache Fulfillment Center Snapshot”] INV –> DB1[(“Inventory Database Sharded by Fulfillment Center”)] CAP –> DB2[(“Capacity Database Sharded by Region”)] OS –> DB3[(“Order Database Primary and Replicas”)] OS –> MQ[“Message Queue Order Events Stream”] MQ –> LB5[“Load Balancer Dispatch Service Cluster”] LB5 –> DISPATCH[“Dispatch Service Assigns Delivery Driver”] MQ –> NOTIFY[“Notification Service SMS Email Push”] MQ –> ANALYTICS[“Analytics Pipeline Stream Processing”]
Figure 4.1 — End-to-end architecture: API Gateway, per-service load balancers, routing engine and its dependencies, sharded data stores, and asynchronous downstream consumers.

Let’s walk through each box in this diagram and explain exactly what it does and why it exists.

4.1 API Gateway

The API Gateway is the single entry point for every request coming from mobile apps and web clients. It is responsible for authenticating the customer (checking their login token), enforcing rate limits (so one misbehaving client cannot flood the system), doing coarse request validation, and routing the request to the correct downstream service. Without an API Gateway, every internal service would need to independently implement authentication and rate limiting, which is both wasteful and dangerous — a single missed check in one service becomes a security hole.

4.2 Load Balancer (in front of every service)

Notice that every internal service in the diagram — Order Service, Routing Engine, Inventory Service, Capacity Service, and Dispatch Service — sits behind its own load balancer. This is deliberate. Each of these services runs as a cluster of many identical instances (for scale and for fault tolerance), and the load balancer’s job is to spread incoming requests evenly across healthy instances, and to instantly stop sending traffic to an instance that crashes or becomes slow. A load balancer typically works using a simple algorithm like round robin (send requests to instances one after another in turn) or least-connections (send the next request to whichever instance currently has the fewest active requests), combined with periodic health checks.

4.3 Order Service

The Order Service owns the lifecycle of an order: it validates the request (are the item IDs real, is the address well-formed), creates the order record, and orchestrates the rest of the flow by calling the Routing Engine and, later, publishing events once the order is confirmed.

4.4 Routing Engine

This is the heart of our system. Given an order’s items and delivery address, it identifies candidate fulfillment centers within range, checks each candidate’s inventory and capacity, scores them, and returns the best choice (or a split plan across multiple centers if needed). We will dedicate an entire section to how this works internally.

4.5 Geo Index Service

Before we can check inventory or capacity, we first need to know which fulfillment centers are even geographically plausible. The Geo Index Service maintains a spatial index (commonly a geohash grid or an R-tree structure) over every fulfillment center’s location and serviceable area, so that given a delivery address, it can answer “which centers are within reasonable range” in a few milliseconds, without scanning every center in the country.

4.6 Inventory Service

This service is the single source of truth for how many units of each item exist at each fulfillment center right now. It must support very fast reads (checking stock) and safe, concurrent writes (decrementing stock when a reservation is made), which is why its database is sharded by fulfillment center, so that hot items in one warehouse do not create contention for orders being placed in a completely different city.

4.7 Capacity Service

Capacity tracks, per fulfillment center and per delivery time window, how many more deliveries can be promised. It is conceptually similar to Inventory, except that instead of physical stock it tracks driver-hours and delivery slots, both of which reset daily and cannot be replenished mid-day the way inventory sometimes can be (via restocking).

4.8 Distributed Cache

Since checking every candidate center’s full inventory and capacity on every single request would be slow, the Routing Engine keeps a fast, in-memory snapshot of approximate stock and capacity levels in a distributed cache (such as Redis). This snapshot is used to quickly filter out centers that are obviously unavailable, before doing an authoritative check against the real database only for the few centers still in contention.

4.9 Message Queue and Downstream Consumers

Once an order is confirmed and assigned to a center, the Order Service publishes an event onto a message queue (such as Kafka). This decouples the checkout flow from everything that happens next: the Dispatch Service picks up the event to assign a driver, the Notification Service sends confirmation messages, and the Analytics Pipeline streams the event into reporting systems — all independently, all without slowing down the customer’s checkout experience.

💬
What an interviewer may ask

“Why does every service have its own load balancer instead of one shared load balancer for the whole system?” Good answer: each service scales independently and has different traffic patterns and failure characteristics. A single shared load balancer becomes a bottleneck and a single point of failure, and it prevents each service from being deployed, scaled, and health-checked independently.

05

Internal Working: The Routing Algorithm

This is where the real engineering happens. Let’s walk through, step by step, how the Routing Engine turns “here is an order and an address” into “here is the fulfillment center that should serve it.”

5.1 Step 1: Candidate Generation

The engine first asks the Geo Index Service for every fulfillment center whose serviceable area contains the delivery address. This step is deliberately cheap and approximate — its only job is to shrink the search space from “every warehouse in the country” (which could be hundreds) down to a short list of perhaps five to twenty realistic candidates.

Concretely, this is usually implemented by computing the geohash of the delivery address, then looking up which fulfillment centers have registered coverage over that geohash cell and its immediate neighboring cells (to handle addresses that fall near a cell boundary). Because this lookup only touches a small, precomputed index rather than scanning every center’s full serviceable-area polygon, it typically completes in a few milliseconds even with a network of thousands of centers.

5.2 Step 2: Fast Filtering with the Cache

Against this short list, the engine checks the distributed cache for a quick approximate answer to two questions: does this center likely have all the ordered items in stock, and does it likely have delivery capacity left today? Centers that clearly fail either check are dropped immediately, without touching the authoritative database.

5.3 Step 3: Authoritative Check

For the remaining candidates (usually just a handful), the engine performs an authoritative, strongly consistent check directly against the Inventory Service and Capacity Service databases. This is intentionally the expensive step, but because we already filtered the list down using the cache, it only runs against a small number of centers instead of all of them.

5.4 Step 4: Scoring

Each surviving candidate is scored using a weighted formula that typically combines: drive-time distance (closer is better), remaining capacity (centers with more spare capacity are preferred, to spread load evenly and avoid overloading a single hub), and cost to serve (some centers may have cheaper delivery costs due to route density). A simplified scoring function looks like this:

$$ text{score} = W_{d} cdot tilde d ;+; W_{c} cdot (1 – tilde r) ;+; W_{$} cdot tilde text{cost} $$

where $tilde d$ is normalized distance, $tilde r$ is normalized remaining-capacity ratio, and $tilde{text{cost}}$ is normalized delivery cost. Lower score wins. The weights $W_{d}$, $W_{c}$, $W_{$}$ are tunable business parameters — a company optimizing purely for speed might weigh distance heavily, while one optimizing for cost efficiency might weigh delivery cost more.

5.5 Step 5: Reservation

Once the winning center is chosen, the engine places a short-lived soft hold on the required stock units and a delivery slot at that center, typically with an expiry of a few minutes. This hold is what prevents two simultaneous customers from both being promised the very last unit of an item.

5.6 Step 6: Order Splitting Fallback

If no single center can satisfy every item in the order, the engine falls back to a split plan: it groups items by the smallest number of centers that together cover the full order, and returns multiple shipments instead of one. This is always a fallback, never the first choice, because it increases delivery cost and complexity.

5.7 The routing engine in code

Here is a simplified Java implementation of the core selection logic, using the Haversine formula for distance and a simple weighted score:

FulfillmentRouter.java — candidate scoring & reservation with retry
public class FulfillmentRouter {

    private final GeoIndexClient geoIndex;
    private final InventoryClient inventory;
    private final CapacityClient capacity;

    public FulfillmentRouter(GeoIndexClient geoIndex,
                              InventoryClient inventory,
                              CapacityClient capacity) {
        this.geoIndex = geoIndex;
        this.inventory = inventory;
        this.capacity = capacity;
    }

    public RoutingDecision route(Order order) {
        List<FulfillmentCenter> candidates =
                geoIndex.findCentersServing(order.getDeliveryAddress());

        List<ScoredCenter> viable = new ArrayList<>();

        for (FulfillmentCenter center : candidates) {
            if (!inventory.hasAllItems(center.getId(), order.getItems())) {
                continue;
            }
            if (!capacity.hasOpenSlot(center.getId(), order.getRequestedWindow())) {
                continue;
            }
            double distanceKm = haversineKm(order.getDeliveryAddress(), center.getLocation());
            double remainingCapacityRatio =
                    capacity.remainingRatio(center.getId(), order.getRequestedWindow());
            double deliveryCost = costModel.estimate(center, order);

            double score = (0.5 * normalize(distanceKm, 0, 25))
                          + (0.3 * (1 - remainingCapacityRatio))
                          + (0.2 * normalize(deliveryCost, 0, 500));

            viable.add(new ScoredCenter(center, score));
        }

        if (viable.isEmpty()) {
            return splitAcrossCenters(order, candidates);
        }

        viable.sort(Comparator.comparingDouble(ScoredCenter::getScore));
        FulfillmentCenter winner = viable.get(0).getCenter();

        boolean reserved = inventory.reserve(winner.getId(), order.getItems(), order.getId())
                && capacity.reserveSlot(winner.getId(), order.getRequestedWindow(), order.getId());

        if (!reserved) {
            // Race lost to another order; retry with next best candidate
            viable.remove(0);
            return retryWithRemaining(order, viable, candidates);
        }

        return RoutingDecision.singleCenter(winner);
    }

    private double haversineKm(Address a, GeoLocation b) {
        final double R = 6371.0;
        double lat1 = Math.toRadians(a.getLatitude());
        double lat2 = Math.toRadians(b.getLatitude());
        double dLat = Math.toRadians(b.getLatitude() - a.getLatitude());
        double dLon = Math.toRadians(b.getLongitude() - a.getLongitude());

        double h = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                 + Math.cos(lat1) * Math.cos(lat2)
                 * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
        return R * c;
    }
}

Notice the fallback path: if the reservation step fails because another order won the race in between scoring and reserving, the router simply retries against the next-best candidate rather than failing the whole order. This is a small but important resilience detail — treating a lost race as a normal, expected event rather than an error.

5.8 Capacity as atomic counters

The Capacity Service, which the router above calls into, is typically implemented around a single atomic decrement operation so that concurrent requests for the last remaining delivery slot resolve safely without a heavier locking mechanism:

CapacityService.java — decrement-if-positive slot reservation
public class CapacityService {

    private final AtomicCounterStore counterStore;

    public boolean hasOpenSlot(String centerId, TimeWindow window) {
        long key = counterKey(centerId, window);
        return counterStore.peek(key) > 0;
    }

    public boolean reserveSlot(String centerId, TimeWindow window, String orderId) {
        long key = counterKey(centerId, window);
        // Atomic decrement-if-positive: returns false if counter is already at zero
        boolean decremented = counterStore.decrementIfPositive(key);
        if (decremented) {
            reservationLog.record(centerId, window, orderId, Instant.now().plus(HOLD_TTL));
        }
        return decremented;
    }

    public void release(String centerId, TimeWindow window, String orderId) {
        long key = counterKey(centerId, window);
        if (reservationLog.remove(centerId, window, orderId)) {
            counterStore.increment(key);
        }
    }

    private long counterKey(String centerId, TimeWindow window) {
        return Objects.hash(centerId, window.getStartEpoch(), window.getEndEpoch());
    }
}

Notice the symmetry with the Inventory Service’s optimistic locking approach: both rely on the underlying store’s atomic primitives (compare-and-swap for stock records, decrement-if-positive for capacity counters) rather than application-level locks, which is what allows both services to remain fast under heavy concurrent load.

5.9 Dispatching drivers, asynchronously

Finally, once an order is confirmed, the Dispatch Service consumes the order-created event and matches the order to an available driver, using a similar nearest-candidate approach scoped to that one fulfillment center’s currently active drivers:

DispatchService.java — nearest-driver assignment by detour minutes
public class DispatchService {

    public DriverAssignment assignDriver(String centerId, Order order) {
        List<Driver> availableDrivers = driverRegistry.findAvailable(centerId);

        Driver bestMatch = null;
        double bestScore = Double.MAX_VALUE;

        for (Driver driver : availableDrivers) {
            if (!driver.canFitAdditionalStop(order)) {
                continue;
            }
            double detourMinutes = routeEstimator.estimateAddedDetour(driver.getCurrentRoute(), order);
            if (detourMinutes < bestScore) {
                bestScore = detourMinutes;
                bestMatch = driver;
            }
        }

        if (bestMatch == null) {
            return DriverAssignment.deferred(order.getId());
        }

        driverRegistry.addStop(bestMatch.getId(), order);
        return DriverAssignment.confirmed(order.getId(), bestMatch.getId(), bestScore);
    }
}

This mirrors the routing engine’s overall shape: generate candidates, filter by feasibility, score, and pick the best — the same pattern shows up at multiple layers of this system, from picking a fulfillment center down to picking an individual driver.

📌
Production tip

Real systems rarely use pure straight-line (Haversine) distance for the final decision. They use a routing or drive-time API for the top few candidates only, because drive-time calls are expensive; Haversine is used earlier just to shrink the candidate list cheaply.

06

Data Flow & Order Lifecycle

Let’s trace a single order end to end, first as a sequence of service calls, then as a state machine describing the order’s lifecycle.

sequenceDiagram participant U as Customer App participant GW as API Gateway participant OS as Order Service participant RE as Routing Engine participant INV as Inventory Service participant CAP as Capacity Service participant MQ as Message Queue participant DS as Dispatch Service U->>GW: Place order with address and items GW->>OS: Forward validated request OS->>RE: Request best fulfillment center RE->>INV: Check stock for candidate centers INV–>>RE: Return centers with available stock RE->>CAP: Check delivery capacity for candidates CAP–>>RE: Return centers with open delivery slots RE–>>OS: Return selected fulfillment center OS->>MQ: Publish order created event OS–>>GW: Return order confirmation GW–>>U: Show confirmation and delivery window MQ->>DS: Consume order event DS->>DS: Assign nearest available driver DS->>MQ: Publish dispatch confirmed event
Figure 6.1 — End-to-end sequence: synchronous critical path stops at “center reserved,” and driver assignment happens asynchronously off the message queue.

Notice that the customer receives their confirmation as soon as a center is selected and reserved — they do not have to wait for driver assignment. Driver assignment happens asynchronously in the background, consumed off the message queue by the Dispatch Service. This is a key latency optimization: the checkout flow only waits for the steps that genuinely need to be synchronous.

6.1 Order lifecycle as a state machine

Now let’s look at the full lifecycle of an order as a state machine, including the failure and retry paths that a naive design often forgets:

stateDiagram-v2 [*] –> Placed Placed –> RoutingInProgress RoutingInProgress –> CenterAssigned RoutingInProgress –> RoutingFailed RoutingFailed –> RoutingInProgress CenterAssigned –> Picking Picking –> PackedForDelivery PackedForDelivery –> DriverAssigned DriverAssigned –> OutForDelivery OutForDelivery –> Delivered OutForDelivery –> DeliveryFailed DeliveryFailed –> DriverAssigned Delivered –> [*]
Figure 6.2 — Order lifecycle including failure and retry loops.

A few things worth calling out about this state machine. First, RoutingFailed loops back into RoutingInProgress rather than being a dead end — a failed routing attempt (for example, the chosen center’s reservation lost a race) should trigger an automatic retry against the next-best candidate before ever surfacing a failure to the customer. Second, DeliveryFailed (a missed delivery attempt) loops back to DriverAssigned rather than back to routing — the item is already physically picked and packed, so re-attempting delivery from the same center is far cheaper than re-running the entire routing decision.

💬
What an interviewer may ask

“What happens if the customer’s payment fails after a center has already reserved stock?” The correct answer is that the reservation is time-boxed (a soft hold with a short expiry, such as 5 to 10 minutes), so if payment does not complete in time, the hold automatically releases the stock and capacity back to the pool without any manual cleanup.

07

Databases, Sharding & Replication

The database layer is where correctness under concurrency really gets tested, because thousands of orders may try to modify the same warehouse’s inventory at the same time.

7.1 Sharding Inventory by Fulfillment Center

The single most important database design decision in this system is to shard the inventory database by fulfillment center ID. Since orders for a customer in Mumbai never touch a warehouse in Delhi, there is no reason for their database writes to contend with each other. By partitioning data so that each shard owns the inventory of a small group of geographically nearby centers, we turn what would be one giant, highly contended database into many smaller, mostly independent databases, each handling only the write load relevant to its own region.

flowchart LR subgraph RegionNorth[“Region North”] FC1[“Fulfillment Center 1”] FC2[“Fulfillment Center 2”] end subgraph RegionSouth[“Region South”] FC3[“Fulfillment Center 3”] FC4[“Fulfillment Center 4”] end FC1 –> SH1[(“Inventory Shard 1 Primary”)] FC2 –> SH2[(“Inventory Shard 2 Primary”)] FC3 –> SH3[(“Inventory Shard 3 Primary”)] FC4 –> SH4[(“Inventory Shard 4 Primary”)] SH1 –> SH1R[(“Shard 1 Replica”)] SH2 –> SH2R[(“Shard 2 Replica”)] SH3 –> SH3R[(“Shard 3 Replica”)] SH4 –> SH4R[(“Shard 4 Replica”)] ROUTER[“Shard Router Consistent Hashing on Center ID”] –> SH1 ROUTER –> SH2 ROUTER –> SH3 ROUTER –> SH4
Figure 7.1 — Inventory sharded by fulfillment center with per-shard primary and replica; a consistent-hashing router directs each request to the correct shard.

A shard router sits in front of the database layer and directs every read or write to the correct shard, typically using consistent hashing on the fulfillment center ID, so that adding or removing shards later requires moving only a small fraction of the data rather than reshuffling everything.

7.2 Replication for Read Scale and Durability

Each shard’s primary handles all writes (since inventory decrements must be strictly ordered and consistent), while one or more replicas handle read traffic, such as background stock-level checks and reporting queries that do not need the absolute latest value. Replicas also serve as the failover target if a primary crashes, which we will cover in the reliability section.

7.3 Order Database Design

Unlike inventory, the order database is not naturally partitioned by geography in the same tight sense, since a customer’s order history should be queryable regardless of which center fulfilled it. It is more commonly sharded by customer ID or order ID, with the routing decision (which center was chosen) stored as an attribute on the order record, plus a secondary index for operational queries like “show me all orders assigned to Center 7 today.”

7.4 Capacity Database Design

Capacity is typically modeled as a set of counters, one per fulfillment center per delivery window per day (for example, “Center 12, 2 PM to 4 PM slot, today: 8 of 20 delivery slots remaining”). Because these counters are hot — many concurrent orders try to decrement the same counter during a busy period — capacity data often lives in a fast key-value store with atomic decrement operations rather than a traditional relational table, precisely to avoid the lock contention that a naive read-modify-write pattern would cause.

7.5 Connection Pooling

Opening a fresh database connection for every single request is expensive — the handshake, authentication, and setup involved can take tens of milliseconds, which is unacceptable inside a latency budget already measured in the low hundreds of milliseconds. Instead, each service maintains a connection pool: a fixed set of already-open, reusable database connections that requests borrow and return. A properly sized pool balances two failure modes — too small a pool causes requests to queue up waiting for a free connection during traffic spikes, while too large a pool can overwhelm the database server itself with more concurrent connections than it can efficiently handle. Pool sizes are typically tuned empirically under load testing rather than guessed, and are set independently per service and per shard, since the Inventory Service’s write-heavy connection pattern has very different needs from the Order Service’s more balanced read-and-write pattern.

7.6 Store selection at a glance

Data TypePartition KeyConsistency NeedTypical Store
InventoryFulfillment Center IDStrong (no overselling)Relational DB, sharded, or distributed KV store with transactions
CapacityCenter ID + Time WindowStrong, atomic countersIn-memory KV store with atomic ops (e.g. Redis, or a counter-optimized store)
OrdersOrder ID / Customer IDStrong for writes, eventual for analytics copiesRelational DB with read replicas
Geo IndexGeohash cellEventual (refreshed periodically)Spatial index / R-tree structure
💬
What an interviewer may ask

“Why not shard inventory by product ID instead of by fulfillment center?” A strong answer: a single popular product is sold from many centers, so sharding by product ID would concentrate all writes for that product onto one shard regardless of geography, recreating the hotspot problem. Sharding by center ID naturally spreads load because it aligns with how traffic is already geographically distributed.

08

Caching Strategy

Caching in this system exists purely to reduce load on the authoritative databases and to speed up the candidate-filtering step, never to replace the authoritative check for the final decision.

The Routing Engine keeps a distributed cache (commonly Redis) holding an approximate snapshot of each center’s stock levels and remaining capacity, refreshed every few seconds or updated incrementally via events published whenever inventory or capacity changes. This cache is intentionally allowed to be slightly stale — being off by one or two units for a second or two is an acceptable trade-off, because the final reservation step always re-validates against the authoritative database before committing.

A useful mental model: the cache answers “is this center probably a good candidate,” and the database answers “is this center definitely still available right now.” Using the cache to do the first, cheap filtering pass and the database only for the second, small, expensive pass is what keeps end-to-end latency low without sacrificing correctness.

Cache invalidation follows a write-through pattern for capacity (since capacity changes are relatively infrequent and high-value to keep fresh) and a short time-to-live expiry pattern for inventory snapshots (since inventory changes extremely frequently during flash sales, and trying to write-through every single decrement would create its own bottleneck).

09

APIs & Microservices

The system is deliberately split into small, independently deployable services, each owning one clear responsibility. This is a classic microservices architecture, and it is worth being explicit about the API contracts between them.

POST /v1/orders — customer-facing order creation contract
POST /v1/orders
{
  "customerId": "cust_8213",
  "deliveryAddress": { "lat": 28.6139, "lng": 77.2090 },
  "items": [ { "sku": "SKU-1001", "qty": 1 }, { "sku": "SKU-2003", "qty": 2 } ],
  "requestedWindow": "2026-07-31T14:00:00/2026-07-31T16:00:00"
}

Response 201 Created
{
  "orderId": "ord_99213",
  "status": "CENTER_ASSIGNED",
  "assignedCenterId": "fc_0412",
  "deliveryWindow": "2026-07-31T14:00:00/2026-07-31T16:00:00"
}

Internally, the Order Service calls the Routing Engine over a fast internal protocol such as gRPC rather than REST, because internal service-to-service calls benefit from gRPC’s lower serialization overhead and strongly typed contracts, whereas the customer-facing API benefits from REST’s simplicity and broad client compatibility.

Each service also exposes a narrow, purpose-built API rather than a generic one. The Inventory Service, for example, does not expose a generic “run any query” endpoint; it exposes specific operations like checkAvailability, reserve, confirm, and release, which makes its behavior predictable and easy to reason about, and prevents callers from accidentally coupling themselves to internal table structure.

10

CAP Theorem & Consistency Trade-offs

The CAP theorem states that a distributed data store can only fully guarantee two of the following three properties at the same time during a network partition: Consistency (every read sees the latest write), Availability (every request gets a response), and Partition Tolerance (the system keeps working even if some nodes cannot talk to each other). Since network partitions are a fact of life in any distributed system, the real-world choice is between favoring consistency or favoring availability when a partition happens.

Our system does not make one blanket choice — different components deliberately sit at different points on this spectrum, because they have different correctness requirements:

  • Inventory reservations favor consistency (CP). Overselling the last unit of a product to two different customers is a real business problem — refunds, apologies, and lost trust. So the reservation step is willing to reject or retry a request rather than risk an incorrect answer.
  • Geo index lookups favor availability (AP). If the spatial index is slightly stale (a center’s serviceable area updated a minute ago has not yet propagated everywhere), the worst outcome is a slightly suboptimal candidate list, not a broken order. So this component prefers to always answer, even with slightly old data, rather than fail the request.
  • The cache layer is explicitly AP. It always returns an answer, and correctness is guaranteed downstream by the authoritative reservation step, not by the cache itself.

This mixed approach — strong consistency only where it is genuinely required, and availability everywhere else — is a very common real-world pattern, and recognizing where each trade-off belongs is one of the most valuable system design skills.

💬
What an interviewer may ask

“Where in this system would you accept eventual consistency, and where would you not?” Strong candidates identify inventory reservation as needing strong consistency (to prevent overselling) while explaining that the geo index and cache layers are safe to be eventually consistent because their errors are self-correcting downstream.

11

Concurrency & Consensus: Preventing Overselling

The most dangerous failure mode in this whole system is overselling: promising the same physical unit of inventory to two different customers. Let’s look at how to prevent it.

11.1 Optimistic Locking

A simple and effective approach is optimistic locking using a version number or a compare-and-swap operation. Instead of locking a row for the duration of a transaction (which creates contention and slows everything down), each stock record carries a version number. A decrement operation reads the current stock and version, then writes the new stock only if the version has not changed since it was read; if it has changed (meaning someone else modified it in between), the write is rejected and the caller retries.

reserveUnit — optimistic locking with a version check
public boolean reserveUnit(String centerId, String sku, int qty) {
    StockRecord current = stockRepository.find(centerId, sku);
    if (current.getAvailable() < qty) {
        return false;
    }
    int updatedRows = stockRepository.updateIfVersionMatches(
            centerId, sku,
            current.getAvailable() - qty,
            current.getVersion(),
            current.getVersion() + 1
    );
    return updatedRows == 1; // 0 means someone else won the race; caller should retry
}

This pattern (often implemented as a single atomic UPDATE ... WHERE version = ? SQL statement, or as a compare-and-swap command in a key-value store) means the database itself enforces correctness, and application code simply retries on failure rather than needing distributed locks.

11.2 Atomic Counters for Capacity

Capacity decrements are even simpler, since they are pure numeric counters rather than records with many fields. An atomic decrement-if-positive operation (many key-value stores support this natively) is enough: attempt to decrement the counter, and if it would go below zero, refuse the operation and return failure instead.

11.3 Why Not Distributed Locks Everywhere?

It might seem simpler to just take a distributed lock (using something like a lock service) around every stock update. In practice, this is avoided because locks introduce latency (every request has to wait its turn) and create a risk of the whole system stalling if a lock holder crashes while holding the lock. Optimistic concurrency control scales better under high contention because most requests are not fighting over the exact same record most of the time — only truly hot items (a flash-sale product with limited stock) see heavy retry traffic, and that traffic is naturally self-limiting since once stock hits zero, further requests fail fast instead of retrying forever.

ApproachHow It WorksBest ForDownside
Distributed LockCaller acquires an exclusive lock before reading or writing, releases afterRare, complex multi-step operations touching several records at onceAdds latency; a crashed lock holder can stall other requests until the lock expires
Optimistic Locking (Version Check)Read a version, write only if version unchanged, retry on conflictHigh-frequency single-record updates like stock decrementsRequires the caller to implement retry logic
Atomic Counter OperationsStore-native decrement-if-positive or compare-and-swapSimple numeric counters like capacity slotsOnly works for simple counter-style data, not complex records

11.4 Handling Retry Storms

When a single, extremely popular item is down to its last few units, a naive retry policy (immediately retrying a failed reservation) can create a thundering herd — many requests retrying at almost the same instant, repeatedly colliding with each other. The fix is the same exponential backoff with jitter pattern used elsewhere in the system: each failed attempt waits a randomized, growing interval before retrying, spreading retries out over time instead of concentrating them into a fresh collision every few milliseconds.

12

Performance & Scalability

Same-day delivery routing must operate inside a tight latency budget, typically well under 500 milliseconds end to end, because it sits directly inside the customer’s checkout flow. Let’s look at how the design achieves this at scale.

12.1 Horizontal Scaling

Every service in the architecture — Order Service, Routing Engine, Inventory Service, Capacity Service, Dispatch Service — is stateless and horizontally scalable. Statelessness means any instance can handle any request, which is what makes it possible to add more instances behind a load balancer during a traffic spike (like a flash sale) and remove them afterward without any special coordination.

12.2 Narrowing the Candidate Set Early

The single biggest performance lever in this design is candidate generation: instead of evaluating every fulfillment center in the country for every order, the geo index narrows the search to perhaps five to twenty realistic candidates in the first few milliseconds. Every expensive step afterward (authoritative inventory and capacity checks) only runs against this small set, not the whole network.

12.3 Read Replicas and Caching

By serving cache-friendly, approximate reads from the distributed cache and reserving authoritative database access for only the final small candidate set, the system keeps its most expensive resource — the sharded inventory database — free to handle write-heavy reservation traffic instead of being swamped by read traffic.

12.4 Asynchronous Post-Processing

Driver dispatch, customer notifications, and analytics are all handled asynchronously via the message queue, entirely outside the customer’s checkout latency budget. This means the customer-facing critical path only includes: candidate generation, filtering, authoritative check, and reservation — nothing else.

12.5 A Sample Latency Budget

It helps to make the latency target concrete. A typical end-to-end budget for the synchronous part of routing might look like this:

StepTarget Latency
API Gateway auth and routing~10 ms
Candidate generation (geo index)~15 ms
Cache-based filtering~20 ms
Authoritative inventory and capacity checks (parallel)~60 ms
Scoring and reservation~30 ms
Order creation and event publish~25 ms
Total (approximate)~160 ms

Notice that the authoritative checks against Inventory and Capacity are issued in parallel, not one after another — since neither depends on the other’s result, running them concurrently roughly halves that portion of the latency compared to a naive sequential implementation.

12.6 Load Testing and Capacity Planning

Because demand for same-day delivery spikes sharply around predictable events (festival sales, weekends, weather events that boost grocery demand), capacity planning for this system typically involves load testing against synthetic peak traffic well above historical peaks, combined with auto-scaling policies tied to queue depth and request latency rather than just CPU usage, since a routing engine can be CPU-light but latency-sensitive.

12.7 Cost Optimization

Performance and cost are closely linked in this system, because every routing decision has a real-world delivery cost attached to it — fuel, driver time, and vehicle wear all scale with distance traveled. The scoring function’s cost term exists precisely to let the business trade a small amount of speed for meaningfully lower delivery cost when the difference in distance between two viable candidates is small. On the infrastructure side, cost optimization also means right-sizing each service’s compute resources independently: the Routing Engine, which does light computation but many network calls, benefits from more instances with modest CPU each, while the Analytics Pipeline, which does heavier batch-style processing, benefits from fewer instances with more CPU and memory each. Autoscaling policies are tuned per service rather than applying one blanket rule across the whole fleet, since a one-size-fits-all policy tends to either overprovision the light services or underprovision the heavy ones.

Another cost lever is intelligent request coalescing: during traffic spikes, if many requests for the same geographic area arrive within a tiny time window, the candidate generation step can batch and deduplicate identical geo-index lookups rather than repeating the same spatial query many times over, trading a few milliseconds of batching delay for a meaningful reduction in load on the Geo Index Service during peak periods.

13

High Availability & Failure Recovery

A same-day delivery system cannot afford long outages — every minute of downtime during business hours directly translates into missed same-day cutoffs. Let’s look at how the design tolerates failure.

13.1 Database Failover

Each inventory shard’s primary is paired with one or more replicas. If a primary becomes unreachable, an automated failover process promotes a replica to primary within seconds, using a consensus-based leader election mechanism (many managed database and coordination systems implement this using a protocol such as Raft) so that all nodes agree on exactly one new primary, avoiding the dangerous scenario of two nodes both believing they are the primary at once (known as a split-brain).

13.2 Graceful Degradation

If the Capacity Service becomes temporarily unavailable, a well-designed system does not simply fail every order. Instead, it can fall back to a more conservative policy — for example, assuming reduced capacity and only proceeding with routing decisions that have a wide safety margin — rather than blocking all same-day orders outright. This is the principle of graceful degradation: lose some precision, but keep the system functioning.

13.3 Circuit Breakers

Every service-to-service call in this architecture is wrapped in a circuit breaker. If the Capacity Service starts timing out repeatedly, the circuit breaker “trips” and the Routing Engine stops sending it requests for a short cooldown period, failing fast with a fallback response instead of piling up slow requests that would otherwise cascade into an outage across the whole system.

13.4 Retry with Backoff

Transient failures (a brief network blip, a momentarily overloaded instance) are handled with retries using exponential backoff and jitter — waiting a randomized, increasing amount of time between attempts — so that a fleet of clients retrying at the same moment does not create a synchronized retry storm that makes the outage worse.

13.5 Multi-Region Considerations

For a company operating across a large geography, the entire stack described here is often deployed per-region (or per a small number of regions), so that a regional outage — a data center losing power, for instance — only affects same-day delivery in that region rather than the whole country, and traffic can often be redirected to a nearby region’s infrastructure as an emergency fallback for non-region-specific components like authentication.

13.6 Backup and Disaster Recovery

Beyond live replicas, the system maintains periodic, versioned backups of every database shard, stored in a separate physical location or cloud region from the primary data. Disaster recovery plans define a recovery point objective (how much data loss, measured in time, is acceptable — typically just seconds to minutes given continuous replication) and a recovery time objective (how quickly service must be restored after a catastrophic failure). These targets are periodically tested with actual failover drills, not just documented on paper, because an untested recovery plan often fails in exactly the moment it is needed most.

13.7 Bulkheads: Isolating Failure Domains

Borrowing a term from shipbuilding, a bulkhead pattern partitions resources (such as thread pools or connection pools) so that a problem in one area cannot consume all available resources and starve unrelated traffic. For example, requests to the Capacity Service and requests to the Inventory Service use separate connection pools within the Routing Engine, so that if Capacity Service calls start piling up and consuming connections, Inventory Service calls are unaffected and the Routing Engine can still make partial progress.

💬
What an interviewer may ask

“How would you prevent a slow Capacity Service from taking down the entire checkout flow?” Look for circuit breakers, timeouts, bulkheads, and graceful degradation in the answer — not just “add more servers.”

14

Security

Security in this system spans both classic application security concerns and logistics-specific concerns like protecting sensitive location data.

14.1 Authentication & Authorization

The API Gateway enforces authentication on every request, validating the customer’s session token before any request reaches internal services. Internal service-to-service calls use mutual TLS (both sides of the connection verify each other’s identity with certificates) so that only trusted services within the network can call each other, and a compromised external actor cannot directly call the Inventory Service even if they somehow reach the internal network.

14.2 Rate Limiting & Abuse Prevention

Because the routing endpoint is computationally more expensive than a typical read endpoint (it fans out to several downstream services), the API Gateway applies stricter rate limits on order placement than on simple browsing endpoints, protecting the system against both accidental retry storms from buggy clients and deliberate abuse such as scripted mass-ordering to drain popular inventory.

A common implementation is the token bucket algorithm: each client is assigned a bucket that holds a maximum number of tokens and refills at a steady rate; every request consumes one token, and a request is rejected once the bucket is empty, only becoming allowed again as new tokens trickle back in. This allows short, legitimate bursts of activity while still capping sustained abuse over time.

TokenBucketRateLimiter.java — per-client sustained-rate cap with burst allowance
public class TokenBucketRateLimiter {

    private final int capacity;
    private final double refillTokensPerSecond;
    private double availableTokens;
    private long lastRefillTimestamp;

    public synchronized boolean allowRequest() {
        refill();
        if (availableTokens >= 1.0) {
            availableTokens -= 1.0;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.currentTimeMillis();
        double elapsedSeconds = (now - lastRefillTimestamp) / 1000.0;
        double tokensToAdd = elapsedSeconds * refillTokensPerSecond;
        availableTokens = Math.min(capacity, availableTokens + tokensToAdd);
        lastRefillTimestamp = now;
    }
}

14.3 Protecting Location Data

Delivery addresses and precise geolocation are sensitive personal data. They are encrypted at rest, access to raw address data is restricted to the services that strictly need it (the Geo Index Service and Order Service, not, for example, the Analytics Pipeline, which typically only needs a coarser region label), and audit logs record which internal service accessed which customer’s address and when.

14.4 Preventing Inventory and Capacity Manipulation

Reservation and confirmation endpoints validate that the caller (the authenticated order flow) is entitled to modify the specific order’s state, preventing a malicious actor from directly calling internal reservation APIs to artificially drain a competitor’s — or their own — inventory counters.

14.5 Input Validation

Every external input — item SKUs, quantities, addresses, requested delivery windows — is strictly validated at the edge (API Gateway and Order Service) before being trusted by any downstream service, defending against both malformed data and classic injection-style attacks against the databases underneath.

14.6 DDoS Protection

Because the checkout and routing endpoints are publicly reachable, they are natural targets for large-scale traffic floods. A layered defense is used: network-level filtering absorbs the largest volumetric floods before they ever reach the API Gateway, while the API Gateway itself applies per-client and per-IP rate limits to catch smaller-scale abuse that slips through the network layer.

14.7 Encryption in Transit and at Rest

All external traffic between customer clients and the API Gateway is encrypted using TLS. Internally, service-to-service traffic is also encrypted, and sensitive fields in the databases (delivery addresses, payment references) are encrypted at rest, so that even direct access to a database backup file does not expose customer data in plain form.

15

Monitoring, Logging & Tracing

Operating a system this latency-sensitive and business-critical requires strong observability, so operators can see problems before customers do.

15.1 Key Metrics

MetricWhy It Matters
Routing decision latency (p50, p95, p99)Directly affects checkout speed and conversion
Routing failure rate (no viable center found)Signals coverage gaps or capacity shortages
Reservation race-loss rateIndicates contention on hot items or centers
Capacity utilization per centerDetects overloaded or underused fulfillment centers
Same-day promise fulfillment rateTracks whether promised delivery windows are actually met

15.2 Distributed Tracing

Since a single order touches many services, distributed tracing (attaching a shared trace ID to a request as it flows through the API Gateway, Order Service, Routing Engine, Inventory Service, and Capacity Service) is essential for debugging. When a customer reports a slow or failed checkout, engineers can pull up the exact trace and see precisely which hop was slow or errored, rather than manually correlating separate logs across five services.

15.3 Structured Logging

Every service emits structured (machine-parseable) logs including the order ID, trace ID, and outcome of each decision, so that logs can be queried and aggregated (for example, “show me every routing failure for Center 12 in the last hour”) rather than requiring manual text searching.

15.4 Alerting

Alerts are tied to business-meaningful thresholds, not just raw system metrics — for example, alerting when the routing failure rate for any single region crosses a threshold, which usually indicates a real operational problem (a region running low on capacity or a data center issue) rather than a benign blip.

15.5 Operational Dashboards

Operators typically work from a small number of purpose-built dashboards rather than raw metric explorers. A “network health” dashboard shows every fulfillment center on a map, color-coded by remaining capacity, so an operations team can spot a region trending toward saturation well before it starts failing orders. A “routing health” dashboard tracks latency percentiles and failure rates over time, broken down by region, making it easy to spot a regional regression right after a deployment. These dashboards exist specifically to answer the two questions operators ask most often during an incident: is this a data problem (a real shortage of capacity or stock) or a system problem (a bug or an infrastructure failure), because the two require completely different responses.

16

Deployment & Cloud

Each service is packaged as an independent container and deployed on a container orchestration platform (such as Kubernetes), which handles scheduling instances across machines, restarting crashed instances automatically, and scaling the number of instances up or down based on load.

Deployments follow a rolling update strategy: new versions of a service are rolled out gradually, a few instances at a time, with automated health checks gating progress, so that a bad deployment is caught and rolled back after affecting only a small fraction of traffic rather than the whole fleet at once.

Regional deployment mirrors the business’s geography: the full stack (API Gateway, all services, database shards relevant to that region’s fulfillment centers) is typically deployed per major region, both to keep latency low (serving customers from infrastructure physically near them) and to contain the blast radius of any regional infrastructure failure.

Infrastructure is defined as code (using tools that describe servers, networking, and configuration in version-controlled files) so that environments are reproducible, auditable, and can be recreated identically if a region needs to be rebuilt.

16.1 Canary Releases

For especially sensitive services like the Routing Engine, a canary release strategy is often layered on top of rolling updates: the new version first receives a small slice of real production traffic (perhaps one percent), and only if its error rate and latency match or beat the current version does the rollout continue to the rest of the fleet. This catches subtle regressions — a scoring bug that picks slightly worse centers, for instance — that a health check alone would never detect.

16.2 Feature Flags for Scoring Weights

Because the routing scoring function’s weights are business-tunable, they are typically exposed through a feature-flag or configuration service rather than hardcoded, allowing operators to adjust the balance between distance, capacity, and cost in near real time — for example, temporarily weighting capacity more heavily during a predictable demand spike like a festival sale — without requiring a full code deployment.

17

Design Patterns & Anti-Patterns

17.1 Patterns Used

Pattern

Circuit Breaker

Prevents a failing downstream service from cascading failure upstream by tripping open and short-circuiting further calls for a cooldown period.

Pattern

Event-Driven Architecture

Decouples the synchronous checkout path from asynchronous post-processing (dispatch, notifications, analytics) via a durable message queue.

Pattern

CQRS-like Separation

Reads (cache-backed candidate filtering) are handled differently from writes (authoritative, strongly consistent reservations), optimizing each path for its own requirements.

Pattern

Sharding by Access Pattern

Partitioning inventory by fulfillment center matches how traffic naturally distributes geographically, avoiding cross-shard hotspots.

Pattern

Optimistic Concurrency

Avoids costly distributed locks while still preventing overselling, via version-check compare-and-swap updates that retry on conflict.

Pattern

Saga-Style Compensation

A time-boxed reservation with automatic release acts as a lightweight compensating transaction if a later step (payment) fails.

Pattern

Bulkhead

Isolating resource pools per downstream dependency ensures that one failing dependency cannot starve requests to a healthy one.

Pattern

Strangler Migration

When migrating from a monolithic order system, traffic is gradually redirected service by service, letting old and new run side by side instead of a risky big-bang cutover.

17.2 Anti-patterns to Avoid

Anti-pattern

Single Shared Database

Co-locating inventory, capacity, and order data in one database creates a single point of contention and a single point of failure for the whole system.

Anti-pattern

Synchronous Fan-Out to Every Center

Checking every center in the country on every order instead of narrowing candidates first destroys latency at scale.

Anti-pattern

Trusting the Cache for Reservations

Using cached stock numbers to actually commit a reservation, instead of re-validating against the authoritative store, leads directly to overselling.

Anti-pattern

No Reservation Expiry

Holding stock indefinitely for abandoned checkouts silently starves other customers of inventory that is never actually going to be purchased.

Anti-pattern

Tight Coupling of Routing and Dispatch

Making the customer’s checkout wait for driver assignment unnecessarily inflates checkout latency for no customer benefit.

18

Best Practices & Common Mistakes

18.1 Do

  • Do narrow the candidate set as early and cheaply as possible before running expensive checks.
  • Do treat reservation as a strongly consistent operation even if everything else is eventually consistent.
  • Do make every reservation time-boxed with automatic expiry.
  • Do design the routing engine to degrade gracefully (fall back to fewer, more conservative candidates) rather than fail outright when a downstream dependency is unhealthy.
  • Do keep the customer-facing critical path as short as possible, pushing everything that can be asynchronous onto a message queue.

18.2 Avoid

  • Avoid assuming straight-line distance is good enough for the final decision — always validate with real drive-time data for the top few candidates.
  • Avoid forgetting that capacity, unlike inventory, cannot be replenished mid-day — a capacity bug that oversells delivery slots cannot be fixed by “restocking.”
  • Avoid building the routing scoring function as a black box with hardcoded weights — expose the weights as configuration so the business can tune the balance between speed, cost, and load distribution without a code deployment.
  • Avoid ignoring order splitting until it becomes an emergency fix — design for it from day one, since any growing catalog will eventually have items that are not co-located in the same center.

18.3 Testing Strategy

A system this dependent on correct behavior under concurrency needs more than ordinary unit tests. Unit tests still cover individual pieces of logic, such as the scoring function or the Haversine distance calculation, in isolation. Integration tests verify that the Order Service, Routing Engine, Inventory Service, and Capacity Service correctly cooperate end to end for a realistic order. Beyond these, concurrency tests deliberately fire many simultaneous reservation attempts at the same low-stock item in a test environment to confirm that the optimistic locking logic never allows more reservations than available stock — a class of bug that ordinary sequential tests simply cannot catch, since the failure only appears under genuine simultaneous access.

Chaos engineering practices are also common at this scale: deliberately injecting failures — killing a service instance, introducing artificial network latency between two services, or forcing a database failover — into a controlled environment (and sometimes carefully into production itself) to verify that circuit breakers, retries, and graceful degradation behave as designed, rather than assuming they work simply because the code looks correct on paper.

19

Real-World Examples

Several major companies operate systems that closely resemble the design in this tutorial, each with its own emphasis.

Global Marketplace

Amazon

Operates one of the largest fulfillment networks in the world, and its promise-date logic famously factors in inventory position across many warehouse types (large fulfillment centers, smaller sortation centers, and delivery stations) along with carrier capacity, to decide both which warehouse ships an item and what delivery date to promise the customer before they even complete checkout.

Big-Box Retail

Walmart

Leverages its dense network of physical stores as micro-fulfillment points in addition to dedicated warehouses, meaning its routing decision often has to choose between “ship from a distribution center” and “pick from the shelves of the nearest store,” a more complex candidate space than a pure warehouse network.

Grocery Delivery

Instacart

Deals with an especially tight capacity constraint, since grocery orders are picked by a human shopper walking a physical store, meaning capacity is bound not just by delivery drivers but by available in-store shopper time slots as well.

On-Demand Delivery

DoorDash & Uber Eats

Extend a similar routing philosophy down to the level of individual restaurants or dark stores, matching an order to the nearest one with available prep capacity, then separately matching that order to the nearest available courier — effectively two chained routing decisions rather than one.

Indian Marketplace

Flipkart & BigBasket

Operating in the Indian market, they face a particularly wide range of serviceable-area shapes, from dense metro neighborhoods with many small dark stores just a couple of kilometers apart, to smaller towns served by a single larger regional warehouse — meaning their geo index and candidate generation logic must handle both extremely dense and extremely sparse fulfillment networks within the same system.

Quick Commerce

Blinkit, Zepto, Swiggy Instamart

Represent the extreme end of the same-day delivery spectrum: quick-commerce platforms promising delivery in ten to twenty minutes rather than hours. Their routing engines operate on a much smaller effective serviceable radius per dark store and an even tighter latency budget for the routing decision itself, since the entire delivery window is so short that even a routing decision taking a few hundred extra milliseconds is a meaningful fraction of the total promise.

📌
The common shape

Across all of these, the underlying pattern is the same one we designed in this tutorial: narrow a large network down to viable candidates, score them on a mix of distance, availability, and cost, reserve resources safely under concurrency, and hand off everything non-urgent to asynchronous processing.

20

Advantages, Disadvantages & Trade-offs

Advantage

Horizontal Scale

Every service and database shard scales independently, so the system grows in the dimension that’s under pressure rather than requiring the whole stack to grow together.

Advantage

Natural Hotspot Avoidance

Sharding by fulfillment center matches natural traffic patterns geographically, so no single shard becomes the bottleneck for orders in a hot region.

Advantage

Graceful Degradation

Circuit breakers, bulkheads, and conservative fallback policies keep checkout functioning even when a single downstream service is degraded or partially unavailable.

Advantage

Low Customer-Facing Latency

Asynchronous post-processing (dispatch, notifications, analytics) keeps the customer-facing critical path lean, well under a few hundred milliseconds end to end.

Advantage

Lock-Free Concurrency

Optimistic locking avoids costly distributed locking under high contention, letting most requests proceed unblocked and only truly racing writes pay a retry cost.

Trade-off

Operational Complexity

Many moving services to deploy, monitor, and debug; this design is a poor fit for very small companies whose scale does not justify the operational overhead.

Trade-off

Stale Cache Window

The approximate caching layer introduces a small window of stale data; correctness only holds because the authoritative reservation step always re-validates before committing.

Trade-off

Order Splitting Cost

Splitting improves fulfillment rate when no single center has everything, but increases delivery cost and customer-facing complexity (multiple parcels arriving separately).

Trade-off

Multi-Region Overhead

Multi-region deployment adds infrastructure and data-consistency overhead in exchange for reduced blast radius and lower geographic latency.

Trade-off

Ongoing Weight Tuning

Tuning the scoring function’s weights requires ongoing business input, not a one-time engineering decision, so a healthy config surface must be maintained.

21

Frequently Asked Questions

Q1

Why not use a single global database instead of sharding by fulfillment center?

A single global database becomes a write bottleneck and a single point of failure as order volume grows. Sharding by fulfillment center distributes both load and risk, since most orders only ever touch one or two nearby shards.

Q2

How does the system avoid promising a delivery window it cannot keep?

By checking real, authoritative capacity (not just cached estimates) before confirming the order, and by time-boxing reservations so unconfirmed holds do not silently consume capacity that could go to a paying customer.

Q3

What happens during a flash sale when thousands of customers want the same item?

Optimistic concurrency control lets most requests proceed without blocking, and only the small fraction genuinely racing for the last unit experience a retry-or-fail outcome, keeping the system responsive for everyone else.

Q4

Is this architecture over-engineered for a small company?

Yes, for a company with one warehouse and modest order volume, most of this complexity (sharding, multi-region deployment, separate services) is unnecessary. This design is aimed at the scale where a single warehouse and a monolithic checkout flow can no longer keep up with order volume or geographic coverage.

Q5

How do you handle a fulfillment center going offline unexpectedly (for example, a power outage)?

Operationally, the center is marked unavailable in the Capacity Service, which immediately removes it from candidate consideration for new orders, while already-confirmed orders assigned to it are re-routed through the same routing engine to the next best available center.

Q6

How is this different from a general ride-hailing matching system?

Ride-hailing matches a single rider to a single driver, a one-to-one match with no inventory constraint. Same-day delivery routing must additionally verify that the chosen location actually holds every item the order needs, making it a combined inventory-and-capacity matching problem rather than a pure proximity matching problem.

Q7

Why use both a cache and an authoritative database instead of just making the database fast enough?

Even a very fast database has a floor on latency once you add network round trips and query planning, and it has a ceiling on how many concurrent reads it can serve before contention slows writes. The cache absorbs the bulk of read-heavy candidate filtering traffic, leaving the database free to focus on the smaller, more critical volume of authoritative checks and writes.

Q8

What happens if two customers order the last unit at the exact same millisecond?

Both requests attempt an optimistic-locking update against the same stock record. The underlying database guarantees only one of the two concurrent updates succeeds; the other fails the version check, and the routing engine for that losing request automatically retries against the next-best candidate center rather than surfacing an error to the customer.

22

Summary & Key Takeaways

📌
Key takeaways
  • Same-day delivery routing is fundamentally a real-time, multi-constraint optimization problem: distance, inventory, and capacity must all be satisfied simultaneously, under a tight latency budget.
  • The architecture narrows a large candidate space early (via a geo index and cache) before running expensive, authoritative checks against only a handful of realistic candidates.
  • Every service sits behind its own load balancer and scales independently, with the API Gateway as the single, secured entry point for all external traffic.
  • Sharding inventory by fulfillment center — rather than by product — aligns the database layer with real traffic patterns and avoids unnecessary contention.
  • Strong consistency is reserved for the operations that truly need it (inventory and capacity reservation), while everything else favors availability and eventual consistency.
  • Resilience patterns — circuit breakers, graceful degradation, time-boxed reservations, and asynchronous post-processing — are what keep the system usable during partial failures and traffic spikes, not just raw scaling.
  • Real companies (Amazon, Walmart, Instacart, DoorDash) all implement variations of this same underlying pattern, tuned to their own network shape and delivery model.

22.1 The one idea to remember

If you take away one idea from this entire tutorial, let it be this: same-day delivery routing is not one hard problem, it is several moderately hard problems layered on top of each other — fast geographic search, safe concurrent inventory management, capacity-aware scheduling, and resilient service orchestration — and the art of the design is in choosing, for each layer, the right consistency model, the right data partitioning strategy, and the right failure-handling behavior for that layer’s specific correctness requirements. Get those choices right, and the system quietly does its job, turning a customer’s simple “buy now” click into a coordinated, split-second decision across an entire physical network, every single time.