Skip to content

Set Up Multi-Warehouse Fulfillment

⏱ 60 minutes intermediate
📜Corecommerce

By the end of this tutorial, you will have a fulfillment system that:

  • Manages inventory across multiple warehouse locations
  • Routes orders to the optimal warehouse based on proximity and stock
  • Splits orders across warehouses when a single location cannot fulfill completely
  • Tracks inventory reservations during checkout to prevent overselling
  • Provides real-time availability status on product pages

Organizations with multiple warehouses, distribution centers, or retail locations need fulfillment logic that goes beyond single-location stock tracking. This tutorial builds that logic step by step.

Ensure your Commerce instance has products with variants in the catalog. You will configure warehouses, assign inventory, and build allocation rules on top of your existing catalog structure.

Define each physical location where you store and ship inventory.

  1. Navigate to Commerce > Administration > Warehouses
  2. Click New Warehouse for each location
  3. Enter the warehouse name, code, and physical address
  4. Set the fulfillment priority (lower number = higher priority)
  5. Mark each warehouse as active
Create warehouses programmatically
csharp
using Mediachase.Commerce.Inventory;

public void CreateWarehouses(
    IWarehouseRepository warehouseRepo)
{
    var warehouses = new[]
    {
        new Warehouse {
            Code = "EAST-DC",
            Name = "East Coast Distribution Center",
            City = "Newark", State = "NJ",
            PostalCode = "07102",
            IsActive = true,
            IsFulfillmentCenter = true,
            IsPickupLocation = false,
            SortOrder = 1
        },
        new Warehouse {
            Code = "WEST-DC",
            Name = "West Coast Distribution Center",
            City = "Ontario", State = "CA",
            PostalCode = "91761",
            IsActive = true,
            IsFulfillmentCenter = true,
            IsPickupLocation = false,
            SortOrder = 2
        },
        new Warehouse {
            Code = "CENTRAL-DC",
            Name = "Central Distribution Center",
            City = "Dallas", State = "TX",
            PostalCode = "75201",
            IsActive = true,
            IsFulfillmentCenter = true,
            IsPickupLocation = true,
            SortOrder = 3
        }
    };

    foreach (var wh in warehouses)
        warehouseRepo.Save(wh);
}

Distribute stock across your warehouses. Each variant can have different quantities at each location.

Set inventory per warehouse
csharp
public void DistributeInventory(
    IInventoryService inventoryService)
{
    var records = new[]
    {
        new InventoryRecord {
            CatalogEntryCode = "SKU-1001",
            WarehouseCode = "EAST-DC",
            PurchaseAvailableQuantity = 500,
            PurchaseAvailableUtc = DateTime.UtcNow,
            IsTracked = true
        },
        new InventoryRecord {
            CatalogEntryCode = "SKU-1001",
            WarehouseCode = "WEST-DC",
            PurchaseAvailableQuantity = 300,
            PurchaseAvailableUtc = DateTime.UtcNow,
            IsTracked = true
        },
        new InventoryRecord {
            CatalogEntryCode = "SKU-1001",
            WarehouseCode = "CENTRAL-DC",
            PurchaseAvailableQuantity = 200,
            PurchaseAvailableUtc = DateTime.UtcNow,
            BackorderAvailableQuantity = 100,
            BackorderAvailableUtc =
                DateTime.UtcNow.AddDays(7),
            IsTracked = true
        }
    };

    inventoryService.Save(records);
}

Step 3: Build the warehouse selection strategy

Section titled “Step 3: Build the warehouse selection strategy”

Create a strategy that selects the best warehouse for each order line based on proximity to the shipping address and available stock.

WarehouseSelector.cs
csharp
public class ProximityWarehouseSelector
    : IWarehouseSelector
{
    private readonly IWarehouseRepository _warehouses;
    private readonly IInventoryService _inventory;
    private readonly IGeocodingService _geo;

    public ProximityWarehouseSelector(
        IWarehouseRepository warehouses,
        IInventoryService inventory,
        IGeocodingService geo)
    {
        _warehouses = warehouses;
        _inventory = inventory;
        _geo = geo;
    }

    public WarehouseAllocation SelectWarehouse(
        string variantCode,
        decimal quantity,
        Address shippingAddress)
    {
        var available = _inventory
            .GetStock(variantCode)
            .Where(s => s.PurchaseAvailableQuantity > 0)
            .ToList();

        // Sort by distance to shipping address
        var ranked = available
            .Select(s => new {
                Stock = s,
                Warehouse = _warehouses
                    .Get(s.WarehouseCode),
                Distance = _geo.CalculateDistance(
                    s.WarehouseCode,
                    shippingAddress.PostalCode)
            })
            .OrderBy(x => x.Distance)
            .ToList();

        // Find first warehouse that can fill entirely
        var fullFill = ranked.FirstOrDefault(
            x => x.Stock.PurchaseAvailableQuantity
                >= quantity);

        if (fullFill != null)
            return new WarehouseAllocation(
                fullFill.Stock.WarehouseCode, quantity);

        // Otherwise, split across warehouses
        return AllocateAcrossWarehouses(
            ranked, quantity);
    }
}

When no single warehouse can fulfill an order line, split it across multiple locations.

Split allocation logic
csharp
public List<WarehouseAllocation>
    AllocateAcrossWarehouses(
        List<RankedWarehouse> ranked,
        decimal requestedQty)
{
    var allocations = new List<WarehouseAllocation>();
    var remaining = requestedQty;

    foreach (var wh in ranked)
    {
        if (remaining <= 0) break;

        var allocateQty = Math.Min(
            remaining,
            wh.Stock.PurchaseAvailableQuantity);

        allocations.Add(new WarehouseAllocation(
            wh.Stock.WarehouseCode, allocateQty));

        remaining -= allocateQty;
    }

    if (remaining > 0)
    {
        // Check backorder availability
        var backorderWh = ranked.FirstOrDefault(
            x => x.Stock.BackorderAvailableQuantity
                >= remaining);

        if (backorderWh != null)
        {
            allocations.Add(new WarehouseAllocation(
                backorderWh.Stock.WarehouseCode,
                remaining,
                isBackorder: true));
        }
    }

    return allocations;
}

Prevent overselling by reserving inventory when items are added to the cart, then releasing the reservation if the cart expires.

Inventory reservation
csharp
public class InventoryReservationService
{
    private readonly IInventoryService _inventory;
    private readonly TimeSpan _reservationTimeout
        = TimeSpan.FromMinutes(15);

    public ReservationResult Reserve(
        string variantCode,
        string warehouseCode,
        decimal quantity,
        string cartId)
    {
        var request = new InventoryRequest
        {
            CatalogEntryCode = variantCode,
            WarehouseCode = warehouseCode,
            RequestedQuantity = quantity,
            OperationType = InventoryOperationType.Request,
            ExpirationDate = DateTime.UtcNow
                .Add(_reservationTimeout),
            ContextId = cartId
        };

        var response = _inventory.Request(request);

        return new ReservationResult
        {
            IsReserved = response.IsSuccess,
            ReservedQuantity = response.QuantityFilled,
            ShortfallQuantity = quantity
                - response.QuantityFilled,
            ExpiresAt = request.ExpirationDate
        };
    }

    public void Release(string cartId)
    {
        _inventory.CancelOperation(cartId);
    }
}

Show availability status on product pages, aggregated across all warehouses.

Availability display logic
csharp
public class AvailabilityService
{
    private readonly IInventoryService _inventory;

    public ProductAvailability GetAvailability(
        string variantCode)
    {
        var allStock = _inventory
            .GetStock(variantCode)
            .Where(s => s.IsTracked)
            .ToList();

        var totalAvailable = allStock
            .Sum(s => s.PurchaseAvailableQuantity);
        var totalBackorder = allStock
            .Sum(s => s.BackorderAvailableQuantity);

        return new ProductAvailability
        {
            Status = totalAvailable > 0
                ? "In Stock"
                : totalBackorder > 0
                    ? "Available on Backorder"
                    : "Out of Stock",
            TotalAvailable = totalAvailable,
            EarliestShipDate = totalAvailable > 0
                ? DateTime.UtcNow.AddDays(1)
                : allStock
                    .Where(s => s.BackorderAvailableQuantity > 0)
                    .Min(s => s.BackorderAvailableUtc),
            WarehouseCount = allStock
                .Count(s => s.PurchaseAvailableQuantity > 0)
        };
    }
}

Step 7: Configure fulfillment routing rules

Section titled “Step 7: Configure fulfillment routing rules”

Define rules that control which warehouses can fulfill orders for specific regions or shipping methods.

  1. Navigate to Commerce > Administration > Fulfillment
  2. Create routing rules that map shipping regions to warehouses
  3. Set priority order for each region
Shipping regionPrimary warehouseSecondaryTertiary
Northeast USEAST-DCCENTRAL-DCWEST-DC
Southeast USEAST-DCCENTRAL-DCWEST-DC
Central USCENTRAL-DCEAST-DCWEST-DC
Western USWEST-DCCENTRAL-DCEAST-DC

Step 8: Handle inventory sync from external systems

Section titled “Step 8: Handle inventory sync from external systems”

If your warehouses use a WMS (warehouse management system), set up an integration to keep Commerce inventory in sync.

Inventory sync endpoint
csharp
[ApiController]
[Route("api/inventory")]
public class InventorySyncController : ControllerBase
{
    private readonly IInventoryService _inventory;

    [HttpPost("sync")]
    [Authorize(Policy = "InventorySync")]
    public IActionResult SyncInventory(
        [FromBody] InventorySyncRequest request)
    {
        var records = request.Items.Select(item =>
            new InventoryRecord
            {
                CatalogEntryCode = item.Sku,
                WarehouseCode = item.WarehouseCode,
                PurchaseAvailableQuantity =
                    item.AvailableQuantity,
                PurchaseAvailableUtc = DateTime.UtcNow,
                IsTracked = true
            }).ToList();

        _inventory.Save(records);

        return Ok(new {
            Updated = records.Count,
            Timestamp = DateTime.UtcNow
        });
    }
}

Set up alerts for low stock and out-of-stock conditions across your warehouse network.

Low stock monitoring
csharp
public class InventoryMonitor
{
    private readonly IInventoryService _inventory;
    private readonly INotificationService _notifications;

    public async Task CheckLowStockLevels()
    {
        var allStock = _inventory.GetAllStock();

        var lowStockItems = allStock
            .Where(s => s.PurchaseAvailableQuantity > 0
                && s.PurchaseAvailableQuantity
                    <= s.ReorderMinQuantity)
            .GroupBy(s => s.WarehouseCode)
            .ToList();

        foreach (var warehouse in lowStockItems)
        {
            await _notifications.SendAlert(
                $"Low stock: {warehouse.Count()} items " +
                $"at {warehouse.Key}",
                warehouse.Select(s => new {
                    s.CatalogEntryCode,
                    s.PurchaseAvailableQuantity,
                    s.ReorderMinQuantity
                }));
        }
    }
}

Verify the complete fulfillment flow across scenarios.

ScenarioExpected result
Order from NY, stock at EAST-DCFulfilled from EAST-DC
Order from LA, stock at WEST-DCFulfilled from WEST-DC
Order exceeds single warehouse stockSplit across two warehouses
All warehouses out of stockBackorder allocation or out-of-stock message
Concurrent orders for same itemReservation prevents overselling
WMS sync updates stock levelsCommerce inventory reflects WMS data
Stock drops below reorder minimumLow stock alert triggered

Test with concurrent sessions to verify that inventory reservations prevent overselling. Place two orders simultaneously for a variant with limited stock and confirm that only one succeeds while the other receives a stock shortage notification.