Multi Source Inventory replaced the old single-stock Magento inventory system. MSI introduces Sources (physical locations), Stocks (virtual aggregations), and Source Selection Algorithms (how to fulfil orders). Most shops use only default configurations, but once you have multiple warehouses or need custom fulfilment logic, MSI requires deep understanding. I show the architecture and how to write a custom SSA.
MSI concepts
- Source – a physical location with inventory: warehouse, shop, supplier
- Source Item – quantity of a specific SKU at a specific Source
- Stock – a virtual pool of Sources assigned to a sales channel (website)
- Source Selection Algorithm (SSA) – decides which Sources to deduct from when an order is placed
- Salable Quantity – aggregated available quantity for a SKU in a Stock
CLI – checking MSI state
# List all sources
bin/magento inventory:source:list
# List all stocks
bin/magento inventory:stock:list
# Assign source to stock
bin/magento inventory:stock-source-link:assign-stock-to-source \
--stockId=1 --sourceCode=warehouse_wroclaw
# Check salable quantity for a SKU
bin/magento inventory:reservations:cleanup # clean old reservations
Programmatic inventory management
<?php
declare(strict_types=1);
use Magento\InventoryApi\Api\Data\SourceItemInterface;
use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory;
use Magento\InventoryApi\Api\SourceItemsSaveInterface;
class InventoryUpdateService
{
public function __construct(
private SourceItemInterfaceFactory $sourceItemFactory,
private SourceItemsSaveInterface $sourceItemsSave,
private \Magento\InventoryApi\Api\GetSourceItemsBySkuInterface $getSourceItems
) {}
public function updateStock(string $sku, string $sourceCode, float $qty): void
{
$sourceItem = $this->sourceItemFactory->create();
$sourceItem->setSourceCode($sourceCode);
$sourceItem->setSku($sku);
$sourceItem->setQuantity($qty);
$sourceItem->setStatus($qty > 0 ? 1 : 0);
$this->sourceItemsSave->execute([$sourceItem]);
}
public function getTotalQty(string $sku): float
{
$total = 0.0;
foreach ($this->getSourceItems->execute($sku) as $item) {
if ($item->getStatus() === SourceItemInterface::STATUS_IN_STOCK) {
$total += $item->getQuantity();
}
}
return $total;
}
}
Custom Source Selection Algorithm
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\SourceSelection;
use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterface;
use Magento\InventorySourceSelectionApi\Api\SourceSelectionServiceInterface;
use Magento\InventorySourceSelectionApi\Model\SourceSelectionInterface;
// Custom SSA: prefer the source closest to the shipping address
class ClosestSourceAlgorithm implements SourceSelectionInterface
{
public function __construct(
private \Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesForStock,
private \Vendor\Module\Model\DistanceCalculator $distanceCalc,
private \Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterfaceFactory $resultFactory,
private \Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionItemInterfaceFactory $itemFactory
) {}
public function execute(InventoryRequestInterface $inventoryRequest): SourceSelectionResultInterface
{
$stockId = $inventoryRequest->getStockId();
$sources = $this->getSourcesForStock->execute($stockId);
$shippingAddr = $inventoryRequest->getExtensionAttributes()?->getDestinationAddress();
// Sort sources by distance to shipping address
$sortedSources = $this->sortByDistance($sources, $shippingAddr);
$selectionItems = [];
$isShippable = true;
foreach ($inventoryRequest->getItems() as $requestItem) {
$sku = $requestItem->getSku();
$qtyNeeded = $requestItem->getQty();
$qtyLeft = $qtyNeeded;
foreach ($sortedSources as $source) {
if ($qtyLeft <= 0) break;
$available = $this->getAvailableQty($sku, $source->getSourceCode());
$deduct = min($available, $qtyLeft);
if ($deduct > 0) {
$selectionItems[] = $this->itemFactory->create([
'sourceCode' => $source->getSourceCode(),
'sku' => $sku,
'qtyToDeduct' => $deduct,
'qtyAvailable' => $available,
]);
$qtyLeft -= $deduct;
}
}
if ($qtyLeft > 0) {
$isShippable = false;
}
}
return $this->resultFactory->create([
'sourceSelectionItems' => $selectionItems,
'isShippable' => $isShippable,
]);
}
private function sortByDistance(array $sources, ?object $address): array
{
if ($address === null) return $sources;
usort($sources, fn($a, $b) =>
$this->distanceCalc->calculate($a, $address)
<=>
$this->distanceCalc->calculate($b, $address)
);
return $sources;
}
private function getAvailableQty(string $sku, string $sourceCode): float { return 0.0; }
}
<!-- Register the custom SSA in di.xml -->
<type name="Magento\InventorySourceSelectionApi\Model\SourceSelectionService">
<arguments>
<argument name="sourceSelectionMethods" xsi:type="array">
<item name="closest_source" xsi:type="array">
<item name="code" xsi:type="string">closest_source</item>
<item name="title" xsi:type="string" translatable="true">Closest Source</item>
<item name="model" xsi:type="object">Vendor\Module\Model\SourceSelection\ClosestSourceAlgorithm</item>
</item>
</argument>
</arguments>
</type>
Migrating from legacy stock API
<?php
// OLD API - still works but deprecated since Magento 2.3
use Magento\CatalogInventory\Api\StockRegistryInterface;
$stockItem = $this->stockRegistry->getStockItemBySku('SKU-001');
$qty = $stockItem->getQty();
// NEW MSI API - preferred
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
$salableQty = $this->getSalableQty->execute('SKU-001', $stockId);
// Check if product is salable for a specific stock
use Magento\InventorySalesApi\Api\IsProductSalableInterface;
$isSalable = $this->isProductSalable->execute('SKU-001', $stockId);
Summary
MSI is complex but the complexity maps to real business requirements. Single warehouse shops can use defaults and never touch it. Multi-warehouse operations need to understand Sources, Stocks, and SSA thoroughly. The custom SSA is the most powerful customisation point – it gives complete control over fulfilment logic. Use the new MSI API from the start; the legacy StockRegistry still works but will eventually be removed.
