Omnichannel inventory management — technische architectuur
Terug naar blog

Omnichannel inventory management — technische architectuur

AuthorRuthger Idema
27 juli 20269 min leestijd

Hoe bouw je één voorraad over webshop, marktplaatsen en winkel? Technische architectuur voor omnichannel inventory management: single source of truth, Laravel OMS-middleware, reserveringen en near-real-time sync.

Omnichannel inventory management — technische architectuur

Webshops die op meerdere kanalen verkopen, lopen gemiddeld 15-20% van hun omzet mis door overselling en handmatige voorraadcorrecties. Eén order op Bol.com die je fysieke winkelvoorraad niet kent, één checkout op je webshop terwijl het laatste product al op Amazon gereserveerd staat. Dat zijn geen rand-cases — dat is dagelijkse realiteit zonder goede omnichannel inventory management architectuur.

De oplossing is geen duur platform met een glanzende demo. Het is een doordachte architectuur: één bron van waarheid, een slimme middleware-laag, en near-real-time synchronisatie over alle kanalen. Dit artikel legt uit hoe dat werkt en wanneer welke aanpak de juiste keuze is.


De kern: single source of truth

Bij omnichannel inventory management geldt één principe boven alles: één systeem bepaalt de werkelijke voorraad. Niet je ERP, je webshop én je WMS tegelijk — één. Alles andere is lezer of schrijver, nooit allebei tegelijk.

In de praktijk zijn er drie kandidaten voor die positie:

  • ERP (SAP, Exact, AFAS) — logisch als je al een ERP hebt met WMS-functionaliteit
  • Dedicated OMS (Order Management System) — de juiste keuze bij complexe fulfilment-stromen
  • Custom Laravel-backend — flexibel, goedkoper, en in veel gevallen meer dan krachtig genoeg

Wij zien bij klanten dat een dedicated OMS pas zinvol wordt bij meer dan drie fulfilment-locaties of bij zeer complexe routeringsregels (dropship, split-shipments, prioritering per regio). Voor de meeste webshops met één of twee magazijnen en een winkel is een goed gebouwde Laravel-service de betere keuze qua TCO.


De middleware-laag: waarom je hem nodig hebt

Kanalen praten nooit dezelfde taal. Magento spreekt GraphQL en REST, Bol.com heeft zijn eigen feed-formaat, je kassasysteem een SOAP-endpoint uit 2009. Ze allemaal direct met elkaar laten praten is een integratie-spaghetti die bij elke API-update kapotgaat.

De middleware-laag — in dit geval een Laravel-applicatie — zit er tussenin en doet drie dingen:

  1. Vertalen: één intern inventory-model, externe formaten worden gemapped
  2. Orkestreren: wie mag wanneer schrijven, welke volgorde hebben updates
  3. Bufferen: als een kanaal even niet bereikbaar is, gaan updates niet verloren

Dit is bewust geen microservice-architectuur. Één Laravel-applicatie met duidelijke modules is makkelijker te onderhouden, te debuggen en te deployen dan vijf kleine services met hun eigen databases en queues. Schaal je later op? Dan split je op. Niet eerder.


Reserveringen en oversell voorkomen

Dit is de moeilijkste schakel. Een product kan tegelijk in drie checkouts zitten op drie verschillende kanalen. Als je alleen de daadwerkelijke voorraad bijhoudt, verkoop je hetzelfde item drie keer.

De oplossing: reserveringen als apart concept in je datamodel.

php
// Laravel — inventory transaction model
Schema::create('inventory_transactions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained();
    $table->foreignId('location_id')->constrained();
    $table->enum('type', ['available', 'reserved', 'sold', 'returned', 'adjustment']);
    $table->integer('quantity'); // negatief = aftrek
    $table->string('reference')->nullable(); // order_id, checkout_token
    $table->timestamp('expires_at')->nullable(); // voor checkout-reserveringen
    $table->timestamps();
});

De beschikbare voorraad bereken je dan altijd als:

php
// Beschikbare voorraad = fysieke voorraad - actieve reserveringen
public function getAvailableQuantity(int $productId, int $locationId): int
{
    return DB::table('inventory_transactions')
        ->where('product_id', $productId)
        ->where('location_id', $locationId)
        ->whereIn('type', ['available', 'sold', 'reserved'])
        ->where(function ($q) {
            $q->whereNull('expires_at')
              ->orWhere('expires_at', '>', now());
        })
        ->sum('quantity');
}

Checkout-reserveringen krijgen een expires_at van doorgaans 10-15 minuten. Een background job ruimt verlopen reserveringen op en gooit de voorraad terug in de pot. Zo voorkom je overselling zonder dat je één globale lock nodig hebt.

Belangrijk: gebruik database-transacties met row-level locking (SELECT FOR UPDATE) voor de reserveringslogica. Race conditions zijn hier niet theoretisch — bij een flash sale op meerdere kanalen tegelijk zijn ze vrijwel zeker.

Near-real-time sync: architectuurpatronen

"Real-time" is een marketingterm. Wat je wilt is near-real-time: updates binnen 10-30 seconden over alle kanalen. Dat is technisch haalbaar en business-verantwoord. Echt real-time (sub-seconde) vereist complexe infrastructure die zelden de ROI rechtvaardigt.

Patroon 1: Event-driven met queue

De aanbevolen aanpak voor de meeste setups:

  • Elke inventory-mutatie publiceert een event: InventoryUpdated
  • Laravel Horizon verwerkt de queue en stuurt updates naar kanalen
  • Kanalen bevestigen ontvangst; bij fout: retry met exponential backoff
php
// Event dispatchen na inventory-mutatie
InventoryUpdated::dispatch($productId, $locationId, $newQuantity);

// Listener stuurt naar kanalen
class SyncInventoryToChannels implements ShouldQueue
{
    public function handle(InventoryUpdated $event): void
    {
        foreach ($this->channelRegistry->getActiveChannels() as $channel) {
            $channel->pushInventoryUpdate(
                $event->productId,
                $event->quantity
            );
        }
    }
}

Voordeel: kanalen zijn ontkoppeld. Als Bol.com even down is, blijven events in de queue tot ze alsnog verwerkt kunnen worden.

Patroon 2: Polling per kanaal

Sommige kanalen (met name oudere ERP-koppelingen) ondersteunen geen webhooks. Dan kies je voor scheduled polling: elke 60-300 seconden checken of er wijzigingen zijn.

Dit werkt, maar heeft een nadeel: je laadt je eigen systeem en het externe kanaal onnodig. Gebruik dit alleen als push niet mogelijk is, en bouw een delta-mechanisme in (alleen wijzigingen ophalen, niet de volledige catalogus).

Patroon 3: Webhook-first

Voor kanalen die webhooks ondersteunen (Shopify, WooCommerce, moderne ERP's): zet de middleware als webhook-ontvanger. Externe systemen pushen wijzigingen naar jou, jij verwerkt en distribueert.

Voordeel: minimale latency, geen onnodige polling. Nadeel: je moet idempotentie goed afhandelen (dubbele webhooks zijn normaal).


Kanaalspecifieke uitdagingen

Elk kanaal heeft eigen eigenaardigheden. Hier de belangrijkste:

KanaalSync-methodeTypische latencyVoorbehoud
Eigen webshop (Magento/Shopify)API push< 5 secRate limits: reken op 2-10 req/sec
Bol.comFeed-upload (CSV/EAN)15-60 minGeen echte real-time API voor voorraad
AmazonSP-API feed15-30 minQuota per account, niet per product
Fysieke winkel (POS)Webhook / polling30-120 secAfhankelijk van kassasysteem
Marktplaats.nlFeed60+ minEenvoudiger feed-formaat

Bol.com en Amazon zijn de pijnpunten. Beide platformhebben geen echte real-time inventory API. Je pushes gaan via feed-bestanden die asynchroon verwerkt worden. Reken op 15-60 minuten doorlooptijd. Dat betekent dat je op drukke dagen (Black Friday) een safety stock-buffer moet instellen: verkoop niet tot de laatste eenheid, maar houd er twee of drie achter.


Buffer-strategie: mathematisch onderbouwd

Voor kanalen met hoge latency gebruik je een dynamische safety stock-buffer. De formule is simpel:

Buffer = (gemiddeld dagverkoop × max sync-latency in dagen) × risicofactor

Voorbeeld: je verkoopt gemiddeld 20 stuks per dag, max latency is 1 uur (= 1/24 dag), risicofactor 2.

Buffer = (20 × 1/24) × 22 stuks

Wij zien bij klanten dat een buffer van 1-3 stuks op de meeste producten voldoende is om overselling te voorkomen zonder significant voorraadkapitaal vast te zetten. Alleen voor producten met extreem lage voorraad (< 5 stuks) wordt die buffer relatief zwaar — overweeg die dan tijdelijk van bepaalde kanalen te halen.


Magento en Shopify als verkoopkanaal

Als je Magento of Shopify als primaire webshop gebruikt binnen een omnichannel-setup, zijn er twee architectuurkeuzes:

Optie A: De webshop als master

De webshop is de single source of truth. Alle andere kanalen synchroniseren vanuit de webshop-voorraad. Eenvoudig bij één webshop, maar kwetsbaar als de webshop even down is.

Optie B: De middleware als master

De webshop is één van de kanalen. De Laravel-backend beheert de centrale voorraad en stuurt alle kanalen aan, inclusief de webshop. Robuuster, meer werk om op te zetten.

Voor serieuze omnichannel-operaties kiezen wij altijd voor optie B. Een Magento- of Shopify-webshop mag nooit het bottleneck zijn voor je gehele inventory-architectuur.

Shopify heeft een relatief rijke Inventory API (Inventory Levels per location) die goed integreert. Magento vereist meer custom werk, zeker als je meerdere MSI-sources (Multi Source Inventory) wilt aansturen via API.


Monitoring en observability

Een omnichannel inventory-systeem zonder monitoring is blind vliegen. Minimaal wil je:

  • Sync lag per kanaal: hoe oud zijn de gesynced voorraadcijfers?
  • Failed jobs: hoeveel queue-jobs zijn mislukt en waarom?
  • Oversell events: zijn er orders binnengekomen terwijl de voorraad al op 0 stond?
  • Reserveringen die nooit omgezet worden: duidt op afgebroken checkouts of bugs

Stel alerting in op sync lag > 10 minuten en failed jobs > 5 per uur. Dat zijn de vroege signalen dat er iets structureel misgaat.


Wanneer is dit NIET de juiste aanpak?

Eerlijkheid verdient: niet elke webshop heeft deze architectuur nodig.

Als je op één kanaal verkoopt met een stabiele, lage voorraadrotatie: gebruik de ingebouwde inventory-tools van je platform. Magento MSI, Shopify Locations — prima voor enkelvoudige setups.

Als je twee kanalen hebt met een laag volume (< 50 orders/dag): een eenvoudige cronjob die elk uur synchroniseert is goed genoeg. Geen event-driven architectuur, geen dedicated OMS. Complexiteit moet proportioneel zijn aan het probleem.

De architectuur in dit artikel is relevant zodra je drie of meer kanalen hebt, meer dan 100 orders per dag verwerkt, of een signifikante kans op overselling hebt door hoge vraag op meerdere kanalen tegelijk.


Aan de slag

Omnichannel inventory management is oplosbaar, ook voor middelgrote webshops. De techniek is er. Het vraagt om een heldere architectuurkeuze vooraf — welk systeem is de master, hoe snel moet de sync zijn, wat is je risicotolerantie voor overselling.

Wil je weten of jouw huidige setup schaalbaar is, of hoe een Laravel-middleware jouw kanalen kan verbinden? Neem contact op en we kijken mee.


Veelgestelde vragen

Wat is het verschil tussen een OMS en een WMS?

Een WMS (Warehouse Management System) beheert de fysieke voorraadbewegingen in een magazijn: opslag, picking, packing. Een OMS (Order Management System) orkestreert orders en routing over kanalen en locaties. Voor omnichannel inventory management heb je primair een OMS-functie nodig; een WMS is aanvullend voor complexe magazijnoperaties.

Hoe voorkom ik overselling bij een flash sale op meerdere kanalen?

Combineer twee maatregelen: een checkout-reserveringssysteem met korte TTL (10-15 min) en row-level locking in de database. Aanvullend kun je een safety stock-buffer instellen voor kanalen met hoge sync-latency zoals Bol.com of Amazon. Overweeg bij een flash sale tijdelijk één kanaal als primair te markeren en andere kanalen pas bij te werken na bevestigde verkoop.

Kan ik Magento MSI gebruiken als centrale inventory-laag?

Dat kan, maar is zelden aan te raden als je ook buiten Magento verkoopt. Magento MSI is ontworpen als intern WMS voor Magento-fulfilment. Externe koppelingen vergen veel custom API-werk en je wordt afhankelijk van de performance en beschikbaarheid van Magento voor je hele supply chain. Beter: Magento als kanaal, een aparte service als master.

Hoeveel kost zo'n architectuur om te bouwen?

Dat varieert sterk. Een basisopzet met twee of drie kanalen en een Laravel-middleware: reken op 80-160 uur development. Een volwassen setup met meerdere locaties, eigen OMS-logica en uitgebreide monitoring: 300-600 uur. De ROI is snel zichtbaar — zelfs bij 10 gemiste orders per maand door overselling à €80 gemiddeld orderwaarde, praat je over €9.600 per jaar aan directe omzetderving, plus reputatieschade.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check