Multi Source Inventory (MSI) is one of the largest refactorings in Magento 2 history – introduced in version 2.3. Instead of a single global stock state, products can have stock in multiple sources (warehouses, physical stores, dropshipping). The Source Selection Algorithm decides which source to fulfil the order from. I show the MSI architecture, how the default algorithm works, and how to write a custom one.
MSI architecture – key concepts
| Concept | Description | Example |
|---|---|---|
| Source | Physical location of goods | Warsaw Warehouse, Krakow Store |
| Stock | Virtual pool of sources assigned to a sales channel | Stock “Poland” = WA + KR + GD |
| Source Item | Stock of a specific SKU in a specific source | SKU-001 in WA: qty=50, status=1 |
| Salable Quantity | Available quantity after reservations | 50 – 3 (reservations) = 47 |
| Reservation | Temporary lock on goods when order is placed | -3 at order placed |
| SSA | Source Selection Algorithm – picks sources for shipment | Priority, Distance, Custom |
Checking MSI stock programmatically
<?php
declare(strict_types=1);
use Magento\InventorySalesApi\Api\GetProductSalableQtyInterface;
use Magento\InventorySalesApi\Api\IsProductSalableInterface;
use Magento\InventorySalesApi\Api\IsProductSalableForRequestedQtyInterface;
class StockChecker
{
public function __construct(
private GetProductSalableQtyInterface $getSalableQty,
private IsProductSalableInterface $isProductSalable,
private IsProductSalableForRequestedQtyInterface $isProductSalableForQty,
) {}
public function check(string $sku, int $stockId = 1): array
{
$salableQty = $this->getSalableQty->execute($sku, $stockId);
$isSalable = $this->isProductSalable->execute($sku, $stockId);
return [
'sku' => $sku,
'stock_id' => $stockId,
'salable_qty' => $salableQty,
'is_salable' => $isSalable,
];
}
public function canFulfil(string $sku, float $requestedQty, int $stockId = 1): bool
{
$result = $this->isProductSalableForQty->execute($sku, $stockId, $requestedQty);
return $result->isSalable();
}
}
Source Items – direct access to source stock levels
<?php
declare(strict_types=1);
use Magento\InventoryApi\Api\GetSourceItemsBySkuInterface;
use Magento\InventoryApi\Api\SourceItemsSaveInterface;
use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory;
class SourceItemManager
{
public function __construct(
private GetSourceItemsBySkuInterface $getSourceItemsBySku,
private SourceItemsSaveInterface $sourceItemsSave,
private SourceItemInterfaceFactory $sourceItemFactory,
) {}
public function getBySkus(array $skus): array
{
$result = [];
foreach ($skus as $sku) {
$sourceItems = $this->getSourceItemsBySku->execute($sku);
foreach ($sourceItems as $item) {
$result[$sku][$item->getSourceCode()] = [
'qty' => $item->getQuantity(),
'status' => $item->getStatus(),
];
}
}
return $result;
}
public function setQty(string $sku, string $sourceCode, float $qty): void
{
$sourceItem = $this->sourceItemFactory->create();
$sourceItem->setSku($sku);
$sourceItem->setSourceCode($sourceCode);
$sourceItem->setQuantity($qty);
$sourceItem->setStatus($qty > 0 ? 1 : 0);
$this->sourceItemsSave->execute([$sourceItem]);
}
public function batchUpdate(array $updates): void
{
$sourceItems = [];
foreach ($updates as $update) {
$item = $this->sourceItemFactory->create();
$item->setSku($update['sku']);
$item->setSourceCode($update['source_code']);
$item->setQuantity($update['qty']);
$item->setStatus($update['qty'] > 0 ? 1 : 0);
$sourceItems[] = $item;
}
if (!empty($sourceItems)) {
$this->sourceItemsSave->execute($sourceItems);
}
}
}
Custom Source Selection Algorithm
The default “Priority” SSA fulfils orders from sources by priority (a number in configuration). A custom SSA allows your own logic – such as selecting the nearest warehouse to the delivery address, minimising shipping costs, or preferring sources with excess stock.
<?php
declare(strict_types=1);
namespace Vendor\Inventory\Model\Algorithms;
use Magento\InventorySourceSelectionApi\Api\Data\InventoryRequestInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterface;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionResultInterfaceFactory;
use Magento\InventorySourceSelectionApi\Api\Data\SourceSelectionItemInterfaceFactory;
use Magento\InventorySourceSelectionApi\Model\SourceSelectionInterface;
use Magento\InventoryApi\Api\GetSourcesAssignedToStockOrderedByPriorityInterface;
class MinimizeSourcesAlgorithm implements SourceSelectionInterface
{
public function __construct(
private GetSourcesAssignedToStockOrderedByPriorityInterface $getSourcesOrderedByPriority,
private SourceSelectionResultInterfaceFactory $selectionResultFactory,
private SourceSelectionItemInterfaceFactory $selectionItemFactory,
private \Magento\InventorySourceSelectionApi\Model\GetInStockSourceItemsBySkusAndSortedSource $getInStockItems,
) {}
// Algorithm: fulfil from as few sources as possible
// (minimises shipping costs when shipping from multiple locations)
public function execute(InventoryRequestInterface $inventoryRequest): SourceSelectionResultInterface
{
$stockId = $inventoryRequest->getStockId();
$sources = $this->getSourcesOrderedByPriority->execute($stockId);
$requestItems = $inventoryRequest->getItems();
$result = [];
// First pass: check if one source can handle the whole order
foreach ($sources as $source) {
$sourceCode = $source->getSourceCode();
$canFulfil = true;
foreach ($requestItems as $requestItem) {
$inStockItems = $this->getInStockItems->execute(
[$requestItem->getSku()],
[$sourceCode]
);
$available = 0.0;
foreach ($inStockItems as $item) {
if ($item->getSourceCode() === $sourceCode) {
$available = $item->getQuantity();
break;
}
}
if ($available < $requestItem->getQty()) {
$canFulfil = false;
break;
}
}
if ($canFulfil) {
foreach ($requestItems as $requestItem) {
$result[] = $this->selectionItemFactory->create([
'sourceCode' => $sourceCode,
'sku' => $requestItem->getSku(),
'qtyToDeduct' => $requestItem->getQty(),
'qtyAvailableToDeduct' => $requestItem->getQty(),
]);
}
return $this->selectionResultFactory->create([
'sourceSelectionItems' => $result,
'shippable' => true,
]);
}
}
return $this->selectionResultFactory->create([
'sourceSelectionItems' => $result,
'shippable' => false,
]);
}
}
Registering Custom SSA in di.xml
<?xml version="1.0"?>
<config>
<type name="Magento\InventorySourceSelectionApi\Model\Algorithms\Registry">
<arguments>
<argument name="algorithms" xsi:type="array">
<item name="minimize_sources" xsi:type="array">
<item name="label" xsi:type="string" translate="true">Minimize Sources</item>
<item name="class" xsi:type="string">Vendor\Inventory\Model\Algorithms\MinimizeSourcesAlgorithm</item>
</item>
</argument>
</arguments>
</type>
</config>
Reservations – how they work
<?php
use Magento\InventoryReservationsApi\Model\AppendReservationsInterface;
use Magento\InventoryReservationsApi\Model\ReservationBuilderInterface;
class ReservationManager
{
public function __construct(
private AppendReservationsInterface $appendReservations,
private ReservationBuilderInterface $reservationBuilder,
) {}
public function reserve(string $sku, float $qty, int $stockId, string $metadata): void
{
$reservation = $this->reservationBuilder
->setSku($sku)
->setQuantity(-$qty)
->setStockId($stockId)
->setMetadata($metadata)
->build();
$this->appendReservations->execute([$reservation]);
}
public function compensate(string $sku, float $qty, int $stockId, string $metadata): void
{
$reservation = $this->reservationBuilder
->setSku($sku)
->setQuantity($qty)
->setStockId($stockId)
->setMetadata($metadata)
->build();
$this->appendReservations->execute([$reservation]);
}
}
Summary
MSI separates physical stock sources from virtual pools (Stock) assigned to sales channels. The Source Selection Algorithm is the extension point where custom business logic decides which source to fulfil the order from. Reservations ensure stock consistency without database locking. A custom SSA registered via di.xml appears automatically in the admin panel when creating a shipment. Next post: advanced enums in PHP 8.1+ – backed enums, methods, interfaces.
