ADR-0003. adjustStock atomic với guard forceNonNegative
| Trường | Giá trị |
|---|---|
| Status | Accepted |
| Date | 2026-02-10 |
| Deciders | inventory-team |
| Supersedes | — |
Bối cảnh
- Sale payment success có thể gửi xuống inventory at-least-once.
- Hai client (vd POS + admin) có thể đồng thời cố trừ stock cho cùng một item.
- Pattern ngây thơ
SELECT current; UPDATE current - delta;là race condition kinh điển dưới concurrency. - Một số item phải block oversell hoàn toàn (nguyên liệu thô với
allowOversell=false), số khác phải cho phép (sản phẩm back-order).
Quyết định
InventoryStockRepository.adjustStock({ stockId, adjustOnHand, adjustAvailable, adjustReserved, forceNonNegative }) thực hiện đọc-và-ghi trong một câu UPDATE SQL duy nhất với guard inline:
sql
UPDATE "InventoryStock"
SET quantity_on_hand = quantity_on_hand + $adjust_on_hand,
quantity_available = quantity_available + $adjust_available,
quantity_reserved = quantity_reserved + $adjust_reserved
WHERE id = $stock_id
AND ($forceNonNegative = false
OR (quantity_on_hand + $adjust_on_hand >= 0
AND quantity_available + $adjust_available >= 0
AND quantity_reserved + $adjust_reserved >= 0))
RETURNING quantity_on_hand, quantity_available, quantity_reserved;Khi forceNonNegative=true và bất kỳ quantity sau khi adjust nào sẽ âm, UPDATE match 0 row; repository trả về null. Caller ghi tracking note OVERSELL_BLOCKED và bỏ qua trừ stock im lặng.
Hệ quả
| Pros | Cons |
|---|---|
| Race-free dưới concurrency tùy ý | Caller phải kiểm tra giá trị trả về null và xử lý |
| Một round-trip duy nhất tới DB | Không có lỗi chi tiết: chỉ "guard failed" |
| Hoạt động dưới serializable, read-committed, v.v. | SQL khó đọc hơn cấp ORM |
| Tương thích với PostgreSQL + Drizzle | Tính khả chuyển cross-database tinh tế |
Phương án thay thế đã cân nhắc
| Phương án | Pros | Cons | Lý do từ chối |
|---|---|---|---|
Optimistic locking (cột version + retry) | DB-portable | Bão retry dưới hot contention | Hiệu năng tệ với SKU hot |
Pessimistic SELECT FOR UPDATE rồi UPDATE | Pattern quen thuộc | 2x round-trip; giữ lock lâu hơn | Chậm hơn; cùng độ đúng |
| Mutex cấp ứng dụng (Redis lock) | Cross-DB | Rủi ro stale lock, single-point-of-failure | Mong manh hơn primitive DB |
Tham chiếu
core/src/repositories/inventory/inventory-stock.repository.ts:36-76(adjustStock)inventory/src/common/constants.ts:8(InventoryTrackingNotes.OVERSELL_BLOCKED)inventory/src/services/inventory-worker.service.ts(caller pattern)