Set Up Multi-Warehouse Fulfillment
What you will build
Section titled “What you will build”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.
Before you start
Section titled “Before you start”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.
Step 1: Create warehouse locations
Section titled “Step 1: Create warehouse locations”Define each physical location where you store and ship inventory.
- Navigate to Commerce > Administration > Warehouses
- Click New Warehouse for each location
- Enter the warehouse name, code, and physical address
- Set the fulfillment priority (lower number = higher priority)
- Mark each warehouse as active
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);
} Step 2: Assign inventory to warehouses
Section titled “Step 2: Assign inventory to warehouses”Distribute stock across your warehouses. Each variant can have different quantities at each location.
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.
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);
}
} Step 4: Implement split shipment logic
Section titled “Step 4: Implement split shipment logic”When no single warehouse can fulfill an order line, split it across multiple locations.
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;
} Step 5: Reserve inventory during checkout
Section titled “Step 5: Reserve inventory during checkout”Prevent overselling by reserving inventory when items are added to the cart, then releasing the reservation if the cart expires.
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);
}
} Step 6: Display real-time availability
Section titled “Step 6: Display real-time availability”Show availability status on product pages, aggregated across all warehouses.
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.
- Navigate to Commerce > Administration > Fulfillment
- Create routing rules that map shipping regions to warehouses
- Set priority order for each region
| Shipping region | Primary warehouse | Secondary | Tertiary |
|---|---|---|---|
| Northeast US | EAST-DC | CENTRAL-DC | WEST-DC |
| Southeast US | EAST-DC | CENTRAL-DC | WEST-DC |
| Central US | CENTRAL-DC | EAST-DC | WEST-DC |
| Western US | WEST-DC | CENTRAL-DC | EAST-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.
[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
});
}
} Step 9: Monitor inventory levels
Section titled “Step 9: Monitor inventory levels”Set up alerts for low stock and out-of-stock conditions across your warehouse network.
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
}));
}
}
} Step 10: Test the multi-warehouse system
Section titled “Step 10: Test the multi-warehouse system”Verify the complete fulfillment flow across scenarios.
| Scenario | Expected result |
|---|---|
| Order from NY, stock at EAST-DC | Fulfilled from EAST-DC |
| Order from LA, stock at WEST-DC | Fulfilled from WEST-DC |
| Order exceeds single warehouse stock | Split across two warehouses |
| All warehouses out of stock | Backorder allocation or out-of-stock message |
| Concurrent orders for same item | Reservation prevents overselling |
| WMS sync updates stock levels | Commerce inventory reflects WMS data |
| Stock drops below reorder minimum | Low 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.