PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

MSI – Multi Source Inventory: source selection algorithm, custom SSA

by Henryk Tews / Saturday, 30 May 2026 / Published in Magento 2

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.

About Henryk Tews

What you can read next

Strategy pattern in PHP – and how Magento 2 uses it in pricing
Xdebug – configuration, PHPStorm, debugging Magento plugins

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}