Table of Contents
▼- Kenapa Sistem Inventori Real-Time Itu Penting
- Arsitektur Database untuk Sistem Inventori
- Implementasi Stock Reservation System
- Menangani Concurrent Transactions
- Real-Time Stock Updates dengan WebSocket
- Multi-Warehouse Inventory Management
- Stock Alert dan Low Stock Notification
- Integrasi dengan Marketplace dan Multi Channel
- Reporting dan Analytics Inventory
- Performance Optimization Tips
- Kesalahan Umum yang Harus Dihindari
- Kesimpulan
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!