Memuat...
👋 Selamat Pagi!

Cara Membangun Sistem Manajemen Inventori Real Time untuk Toko Online

Stok tiba-tiba habis padahal masih tampil tersedia? Overselling bikin pelanggan kabur? Pelajari cara membangun sistem inventori real-time yang akurat dan menceg...

Cara Membangun Sistem Manajemen Inventori Real Time untuk Toko Online

Pernahkah Anda mengalami situasi di mana pelanggan memesan produk, tapi ternyata stok sudah habis?

Atau mungkin Anda harus menolak pesanan karena sistem inventori tidak update secara real-time?

Masalah klasik ini dialami hampir semua pemilik toko online, terutama yang menjual di multi-channel seperti website sendiri, marketplace, dan sosial media.

Sistem manajemen inventori yang baik bukan hanya soal mencatat stok masuk dan keluar. Lebih dari itu, sistem harus mampu memberikan data akurat secara real-time, mencegah overselling, dan mengintegrasikan semua channel penjualan dalam satu dashboard terpusat.

Di artikel ini, saya akan membahas cara membangun sistem inventori real-time yang efisien, mulai dari arsitektur database hingga implementasi teknis yang bisa langsung Anda terapkan.

Kenapa Sistem Inventori Real-Time Itu Penting

Bayangkan Anda punya toko online yang ramai. Setiap menit ada puluhan transaksi masuk dari berbagai channel.

Tanpa sistem real-time, Anda akan menghadapi masalah serius:

  • Overselling - Pelanggan membeli produk yang sebenarnya sudah habis
  • Underselling - Produk masih ada tapi sistem menampilkan stok kosong
  • Kehilangan Revenue - Potensi penjualan hilang karena data stok tidak akurat
  • Customer Complaint - Pelanggan kecewa karena pesanan dibatalkan
  • Operasional Chaos - Tim gudang kebingungan karena data tidak sinkron

Menurut data dari berbagai studi e-commerce, overselling bisa menurunkan kepercayaan pelanggan hingga 60% dan meningkatkan churn rate secara signifikan.

Sistem inventori real-time memastikan setiap transaksi langsung mengurangi stok, setiap restock langsung terupdate, dan semua channel mendapat informasi yang sama dalam hitungan detik.

Arsitektur Database untuk Sistem Inventori

Fondasi sistem inventori yang solid dimulai dari desain database yang tepat.

Berikut struktur tabel yang saya rekomendasikan:

Tabel Products

CREATE TABLE products (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    sku VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    category_id INT,
    price DECIMAL(12,2) NOT NULL,
    status ENUM('active', 'inactive', 'discontinued'),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_sku (sku),
    INDEX idx_status (status)
);

Tabel Inventory

CREATE TABLE inventory (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    warehouse_id INT NOT NULL,
    quantity INT NOT NULL DEFAULT 0,
    reserved_quantity INT NOT NULL DEFAULT 0,
    available_quantity INT GENERATED ALWAYS AS (quantity - reserved_quantity) STORED,
    min_stock_alert INT DEFAULT 10,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id),
    INDEX idx_product_warehouse (product_id, warehouse_id),
    INDEX idx_available (available_quantity)
);

Perhatikan kolom available_quantity yang dihitung otomatis.

Ini adalah computed column yang selalu akurat tanpa perlu update manual.

Tabel Inventory Movements

CREATE TABLE inventory_movements (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    warehouse_id INT NOT NULL,
    type ENUM('in', 'out', 'adjustment', 'transfer'),
    quantity INT NOT NULL,
    reference_type VARCHAR(50), -- 'order', 'purchase', 'return'
    reference_id BIGINT,
    notes TEXT,
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id),
    INDEX idx_product (product_id),
    INDEX idx_reference (reference_type, reference_id),
    INDEX idx_created (created_at)
);

Tabel ini mencatat setiap pergerakan stok sebagai audit trail.

Anda bisa tracking siapa yang melakukan perubahan, kapan, dan untuk apa.

Implementasi Stock Reservation System

Salah satu fitur paling krusial adalah sistem reservasi stok.

Ketika pelanggan menambahkan produk ke keranjang atau melakukan checkout, stok harus di-reserve sementara untuk mencegah orang lain membelinya.

Berikut implementasi dalam PHP:

class InventoryService
{
    private $db;
    
    public function reserveStock($productId, $quantity, $orderId)
    {
        $this->db->beginTransaction();
        
        try {
            // Lock row untuk mencegah race condition
            $sql = "SELECT * FROM inventory 
                    WHERE product_id = ? 
                    AND available_quantity >= ?
                    FOR UPDATE";
            
            $inventory = $this->db->prepare($sql);
            $inventory->execute([$productId, $quantity]);
            $stock = $inventory->fetch();
            
            if (!$stock) {
                throw new InsufficientStockException("Stok tidak mencukupi");
            }
            
            // Update reserved quantity
            $updateSql = "UPDATE inventory 
                          SET reserved_quantity = reserved_quantity + ? 
                          WHERE product_id = ?";
            $this->db->prepare($updateSql)->execute([$quantity, $productId]);
            
            // Log movement
            $this->logMovement($productId, -$quantity, 'reservation', $orderId);
            
            $this->db->commit();
            return true;
            
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
    
    public function releaseReservation($productId, $quantity, $orderId)
    {
        $sql = "UPDATE inventory 
                SET reserved_quantity = reserved_quantity - ? 
                WHERE product_id = ? AND reserved_quantity >= ?";
        
        $this->db->prepare($sql)->execute([$quantity, $productId, $quantity]);
        $this->logMovement($productId, $quantity, 'release', $orderId);
    }
    
    public function confirmSale($productId, $quantity, $orderId)
    {
        $this->db->beginTransaction();
        
        try {
            // Kurangi stok fisik dan reserved
            $sql = "UPDATE inventory 
                    SET quantity = quantity - ?,
                        reserved_quantity = reserved_quantity - ?
                    WHERE product_id = ?";
            
            $this->db->prepare($sql)->execute([$quantity, $quantity, $productId]);
            
            $this->logMovement($productId, -$quantity, 'out', $orderId);
            
            $this->db->commit();
            return true;
            
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}

Fungsi FOR UPDATE di query sangat penting untuk mencegah race condition.

Ini memastikan hanya satu transaksi yang bisa mengakses row tersebut dalam satu waktu.

Menangani Concurrent Transactions

Saat traffic tinggi, banyak pelanggan bisa checkout produk yang sama secara bersamaan.

Tanpa penanganan yang tepat, bisa terjadi overselling.

Butuh jasa pembuatan website profesional? KerjaKode menyediakan layanan pembuatan website berkualitas tinggi dengan harga terjangkau. Kunjungi jasa pembuatan website KerjaKode untuk konsultasi gratis dan wujudkan website impian Anda.

Berikut strategi yang saya rekomendasikan:

1. Database Transaction Isolation

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN;
    -- Lock specific row
    SELECT available_quantity FROM inventory 
    WHERE product_id = 123 FOR UPDATE;
    
    -- Validasi dan update
    UPDATE inventory 
    SET reserved_quantity = reserved_quantity + 2
    WHERE product_id = 123 
    AND available_quantity >= 2;
    
COMMIT;

2. Optimistic Locking dengan Version Column

ALTER TABLE inventory ADD COLUMN version INT NOT NULL DEFAULT 0;

-- Saat update, cek version
UPDATE inventory 
SET reserved_quantity = reserved_quantity + 2,
    version = version + 1
WHERE product_id = 123 
AND version = 5  -- version yang dibaca sebelumnya
AND available_quantity >= 2;

Jika affected rows = 0, artinya ada transaksi lain yang sudah update duluan.

Anda perlu retry dengan membaca version terbaru.

3. Redis untuk High-Speed Locking

class RedisInventoryLock
{
    private $redis;
    
    public function acquireLock($productId, $timeout = 5)
    {
        $lockKey = "inventory_lock:{$productId}";
        $lockValue = uniqid(true);
        
        // SET NX (only if not exists) dengan expiry
        $acquired = $this->redis->set(
            $lockKey, 
            $lockValue, 
            ['NX', 'EX' => $timeout]
        );
        
        return $acquired ? $lockValue : false;
    }
    
    public function releaseLock($productId, $lockValue)
    {
        $lockKey = "inventory_lock:{$productId}";
        
        // Lua script untuk atomic delete
        $script = "
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        ";
        
        return $this->redis->eval($script, [$lockKey, $lockValue], 1);
    }
}

Redis memberikan performa locking yang sangat cepat untuk high-traffic scenarios.

Real-Time Stock Updates dengan WebSocket

Untuk memberikan update real-time ke dashboard admin atau frontend, gunakan WebSocket.

Implementasi menggunakan Laravel dengan Laravel WebSockets atau Pusher:

// Event yang di-broadcast
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class StockUpdated implements ShouldBroadcast
{
    public $productId;
    public $availableQuantity;
    public $warehouseId;
    
    public function __construct($productId, $availableQuantity, $warehouseId)
    {
        $this->productId = $productId;
        $this->availableQuantity = $availableQuantity;
        $this->warehouseId = $warehouseId;
    }
    
    public function broadcastOn()
    {
        return new Channel('inventory-updates');
    }
    
    public function broadcastAs()
    {
        return 'stock.updated';
    }
}

Di frontend, listen event ini:

// JavaScript client
import Echo from 'laravel-echo';

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
});

window.Echo.channel('inventory-updates')
    .listen('.stock.updated', (data) => {
        console.log('Stock updated:', data);
        updateProductDisplay(data.productId, data.availableQuantity);
    });

Setiap kali ada perubahan stok, semua user yang sedang melihat produk tersebut langsung mendapat update.

Multi-Warehouse Inventory Management

Jika bisnis Anda punya beberapa gudang, sistem harus bisa tracking stok per lokasi.

Implementasi routing order ke warehouse terdekat:

class WarehouseRouter
{
    public function findOptimalWarehouse($productId, $customerLocation, $quantity)
    {
        // Ambil semua gudang yang punya stok cukup
        $warehouses = DB::table('inventory')
            ->join('warehouses', 'inventory.warehouse_id', '=', 'warehouses.id')
            ->where('inventory.product_id', $productId)
            ->where('inventory.available_quantity', '>=', $quantity)
            ->select('warehouses.*', 'inventory.available_quantity')
            ->get();
        
        if ($warehouses->isEmpty()) {
            return null; // Stok tidak tersedia di semua gudang
        }
        
        // Hitung jarak ke customer
        $optimal = $warehouses->map(function($warehouse) use ($customerLocation) {
            $warehouse->distance = $this->calculateDistance(
                $warehouse->latitude,
                $warehouse->longitude,
                $customerLocation['lat'],
                $customerLocation['lng']
            );
            return $warehouse;
        })->sortBy('distance')->first();
        
        return $optimal;
    }
    
    private function calculateDistance($lat1, $lon1, $lat2, $lon2)
    {
        // Haversine formula
        $earthRadius = 6371; // km
        
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        
        $a = sin($dLat/2) * sin($dLat/2) +
             cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
             sin($dLon/2) * sin($dLon/2);
        
        $c = 2 * atan2(sqrt($a), sqrt(1-$a));
        
        return $earthRadius * $c;
    }
}

Sistem ini otomatis memilih gudang terdekat dengan stok tersedia untuk meminimalkan biaya dan waktu pengiriman.

Stock Alert dan Low Stock Notification

Sistem harus proaktif memberitahu ketika stok menipis.

Implementasi alert system:

class StockAlertService
{
    public function checkLowStock()
    {
        $lowStockItems = DB::table('inventory')
            ->join('products', 'inventory.product_id', '=', 'products.id')
            ->whereRaw('inventory.available_quantity where('products.status', 'active')
            ->select('products.*', 'inventory.available_quantity', 'inventory.min_stock_alert')
            ->get();
        
        foreach ($lowStockItems as $item) {
            $this->notifyStockAlert($item);
        }
    }
    
    private function notifyStockAlert($item)
    {
        // Kirim email ke purchasing team
        Mail::to('[email protected]')->send(
            new LowStockAlert($item)
        );
        
        // Push notification ke admin dashboard
        event(new LowStockDetected($item));
        
        // Log ke system
        Log::warning("Low stock alert", [
            'product' => $item->name,
            'sku' => $item->sku,
            'current_stock' => $item->available_quantity,
            'min_alert' => $item->min_stock_alert
        ]);
    }
}

Jalankan ini sebagai scheduled job di cron:

// Laravel Scheduler (app/Console/Kernel.php)
protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        app(StockAlertService::class)->checkLowStock();
    })->hourly();
}

Integrasi dengan Marketplace dan Multi Channel

Jika Anda jual di Tokopedia, Shopee, Lazada, dan website sendiri, stok harus sinkron di semua channel.

Konsep central inventory dengan sync mechanism:

class ChannelSyncService
{
    private $channels = [
        'tokopedia' => TokopediaAPI::class,
        'shopee' => ShopeeAPI::class,
        'lazada' => LazadaAPI::class,
    ];
    
    public function syncStockToAllChannels($productId, $availableQuantity)
    {
        $product = Product::find($productId);
        
        foreach ($this->channels as $channelName => $apiClass) {
            try {
                $api = new $apiClass();
                
                // Dapatkan mapping SKU untuk channel ini
                $channelSku = $this->getChannelMapping($product->sku, $channelName);
                
                if ($channelSku) {
                    $api->updateStock($channelSku, $availableQuantity);
                    
                    Log::info("Stock synced to {$channelName}", [
                        'sku' => $product->sku,
                        'channel_sku' => $channelSku,
                        'quantity' => $availableQuantity
                    ]);
                }
                
            } catch (Exception $e) {
                Log::error("Failed to sync to {$channelName}: " . $e->getMessage());
                // Retry mechanism bisa ditambahkan di sini
            }
        }
    }
    
    private function getChannelMapping($masterSku, $channelName)
    {
        return DB::table('channel_product_mappings')
            ->where('master_sku', $masterSku)
            ->where('channel', $channelName)
            ->value('channel_sku');
    }
}

Saat ada penjualan dari marketplace, webhook mereka akan hit endpoint Anda untuk kurangi stok central.

Reporting dan Analytics Inventory

Data inventori yang baik memberikan insight berharga untuk bisnis.

Beberapa report yang perlu Anda build:

Stock Velocity Report

SELECT 
    p.sku,
    p.name,
    COUNT(im.id) as total_movements,
    SUM(CASE WHEN im.type = 'out' THEN im.quantity ELSE 0 END) as total_sold,
    AVG(i.available_quantity) as avg_stock_level,
    (SUM(CASE WHEN im.type = 'out' THEN im.quantity ELSE 0 END) / 
     DATEDIFF(NOW(), MIN(im.created_at))) as daily_velocity
FROM products p
JOIN inventory i ON p.id = i.product_id
JOIN inventory_movements im ON p.id = im.product_id
WHERE im.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY p.id
ORDER BY daily_velocity DESC;

Dead Stock Analysis

SELECT 
    p.sku,
    p.name,
    i.available_quantity,
    DATEDIFF(NOW(), MAX(im.created_at)) as days_without_movement,
    (i.available_quantity * p.price) as tied_capital
FROM products p
JOIN inventory i ON p.id = i.product_id
LEFT JOIN inventory_movements im ON p.id = im.product_id AND im.type = 'out'
WHERE i.available_quantity > 0
GROUP BY p.id
HAVING days_without_movement > 90
ORDER BY tied_capital DESC;

Report ini membantu Anda identify produk yang tidak laku dan mengikat modal.

Performance Optimization Tips

Sistem inventori harus cepat, terutama saat traffic tinggi.

Beberapa optimasi yang perlu diterapkan:

1. Database Indexing

-- Composite index untuk query yang sering dijalankan
CREATE INDEX idx_product_warehouse_available 
ON inventory(product_id, warehouse_id, available_quantity);

-- Index untuk audit trail
CREATE INDEX idx_movements_date_type 
ON inventory_movements(created_at, type);

-- Partial index untuk produk aktif
CREATE INDEX idx_active_products 
ON products(status) WHERE status = 'active';

2. Caching Strategy

class CachedInventoryService
{
    private $cache;
    private $inventoryService;
    
    public function getAvailableStock($productId)
    {
        $cacheKey = "inventory:product:{$productId}";
        
        return $this->cache->remember($cacheKey, 300, function() use ($productId) {
            return $this->inventoryService->getAvailableStock($productId);
        });
    }
    
    public function invalidateCache($productId)
    {
        $cacheKey = "inventory:product:{$productId}";
        $this->cache->forget($cacheKey);
        
        // Broadcast update ke semua instance
        event(new InventoryCacheInvalidated($productId));
    }
}

3. Query Optimization

// Bad - N+1 query problem
$products = Product::all();
foreach ($products as $product) {
    $stock = $product->inventory->available_quantity; // Query per produk
}

// Good - Eager loading
$products = Product::with('inventory')->get();
foreach ($products as $product) {
    $stock = $product->inventory->available_quantity; // No extra query
}

Kesalahan Umum yang Harus Dihindari

Dari pengalaman saya membangun sistem inventori untuk berbagai klien, ini kesalahan yang paling sering terjadi:

1. Tidak Ada Transaction Wrapping

Update stok tanpa transaction bisa menyebabkan data inconsistent.

Selalu wrap operasi critical dalam database transaction.

2. Ignore Race Condition

Tidak menggunakan row-level locking di concurrent scenario adalah disaster waiting to happen.

3. Sinkronisasi Tanpa Retry Mechanism

API marketplace bisa down atau timeout. Harus ada queue dan retry mechanism untuk ensure semua channel ter-update.

4. Tidak Ada Audit Trail

Ketika ada discrepancy stok, Anda perlu tau siapa yang melakukan apa dan kapan.

Table inventory_movements adalah wajib.

5. Over-Engineering

Jangan langsung bikin distributed system dengan message queue kalau traffic masih kecil.

Start simple, scale ketika memang butuh.

Kesimpulan

Membangun sistem inventori real-time memang kompleks, tapi fundamental-nya sebenarnya straightforward.

Yang penting adalah desain database yang solid, penanganan concurrency yang benar, dan monitoring yang baik.

Mulai dari MVP sederhana dengan fitur core: tracking stok, reservation, dan basic reporting.

Setelah jalan dan stabil, baru tambahkan fitur advanced seperti multi-warehouse routing, predictive analytics, atau integrasi blockchain untuk supply chain transparency.

Kunci sukses sistem inventori bukan di teknologi fancy yang Anda pakai, tapi di reliability dan akurasi data yang bisa Anda jaga 24/7.

Good luck dengan implementasi sistem inventori Anda!

Ajie Kusumadhany
Written by

Ajie Kusumadhany

Founder & Lead Developer KerjaKode. Berpengalaman dalam pengembangan web modern dengan Laravel, React.js, Vue.js, dan teknologi terkini. Passionate tentang coding, teknologi, dan berbagi pengetahuan melalui artikel.

Promo Spesial Hari Ini!

10% DISKON

Promo berakhir dalam:

00 Jam
:
00 Menit
:
00 Detik
Klaim Promo Sekarang!

*Promo berlaku untuk order hari ini

0
User Online
Halo! 👋
Kerjakode Support Online
×

👋 Hai! Pilih layanan yang kamu butuhkan:

Chat WhatsApp Sekarang