Stock and Price Synchronization Between Website and Marketplaces
When a store sells simultaneously through its website and multiple marketplaces, inventory management becomes critical: if an item sells on the website, stock needs to decrease on Ozon and Wildberries; when new goods arrive, inventory must be updated everywhere. Price synchronization ensures price parity or markup across marketplaces.
System Architecture
Source of Truth (Main Warehouse / Website)
↓
Stock Manager Service
├── Reservation on website order
├── Release on cancellation
└── Replenishment on goods receipt
↓
Sync Queue (Redis)
↓
Marketplace Workers
├── Ozon Worker → Ozon API
├── WB Worker → WB API
└── YM Worker → Yandex.Market API
Available Stock Calculation
class StockCalculator
{
public function getAvailableForMarketplace(int $productId, string $marketplace): int
{
$product = Product::with(['reservations', 'warehouseItems'])->findOrFail($productId);
$totalStock = $product->warehouseItems->sum('quantity');
$reservedSite = $product->reservations()->where('source', 'site')->sum('quantity');
$reservedOther = $product->reservations()->where('source', '!=', $marketplace)->sum('quantity');
// Marketplaces can see no more than 80% of available stock
$available = $totalStock - $reservedSite - $reservedOther;
return max(0, (int)($available * 0.8));
}
}
The 0.8 coefficient protects against situations where multiple marketplaces see the full inventory and simultaneously accept orders.
Synchronization Queue
class StockSyncQueue
{
public function enqueue(int $productId): void
{
// Deduplication: if already in queue — update timer
Redis::setex("sync:pending:{$productId}", 30, 1);
}
public function processQueue(): void
{
// Batching: collect all changes over 30 seconds and send as batch
$keys = Redis::keys('sync:pending:*');
$productIds = array_map(fn($k) => (int)explode(':', $k)[2], $keys);
if (empty($productIds)) return;
Redis::del($keys);
$this->syncToMarketplaces($productIds);
}
}
Price Synchronization
class PriceSyncService
{
private array $marketplacePriceRules = [
'ozon' => ['type' => 'markup', 'value' => 5.0], // +5%
'wb' => ['type' => 'markup', 'value' => 7.0], // +7%
'ym' => ['type' => 'fixed', 'value' => 0], // no markup
];
public function calculateMarketplacePrice(float $basePrice, string $marketplace): float
{
$rule = $this->marketplacePriceRules[$marketplace];
return match($rule['type']) {
'markup' => round($basePrice * (1 + $rule['value'] / 100), 0),
'fixed' => $basePrice + $rule['value'],
default => $basePrice,
};
}
}
Handling Overlapping Orders
class OrderProcessor
{
public function process(Order $order): void
{
DB::transaction(function () use ($order) {
foreach ($order->items as $item) {
$reserved = ProductReservation::create([
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'source' => $order->source, // 'site', 'ozon', 'wb'
'order_id' => $order->id,
]);
// Check if physical stock is not exceeded
$totalReserved = ProductReservation::where('product_id', $item->product_id)->sum('quantity');
$actualStock = WarehouseItem::where('product_id', $item->product_id)->sum('quantity');
if ($totalReserved > $actualStock) {
throw new InsufficientStockException($item->product_id);
}
}
// Queue job to reduce inventory on all platforms
StockSyncJob::dispatch($order->items->pluck('product_id')->unique()->all());
});
}
}
Discrepancy Monitoring
We periodically reconcile actual marketplace inventory with our data:
class StockDiscrepancyChecker
{
public function check(): array
{
$discrepancies = [];
$ozonStocks = $this->ozon->getAllStocks();
foreach ($ozonStocks as $ozonItem) {
$ourStock = $this->calculator->getAvailableForMarketplace($ozonItem['offer_id'], 'ozon');
if (abs($ourStock - $ozonItem['stock']) > 1) {
$discrepancies[] = [
'sku' => $ozonItem['offer_id'],
'our' => $ourStock,
'ozon' => $ozonItem['stock'],
'delta' => $ourStock - $ozonItem['stock'],
];
}
}
return $discrepancies;
}
}
Timeline
Stock and price synchronization system for 2–3 marketplaces: 14–20 business days.







