Skip to content

ADR-0003. adjustStock atomic với guard forceNonNegative

TrườngGiá trị
StatusAccepted
Date2026-02-10
Decidersinventory-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ả

ProsCons
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 DBKhô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 + DrizzleTính khả chuyển cross-database tinh tế

Phương án thay thế đã cân nhắc

Phương ánProsConsLý do từ chối
Optimistic locking (cột version + retry)DB-portableBão retry dưới hot contentionHiệu năng tệ với SKU hot
Pessimistic SELECT FOR UPDATE rồi UPDATEPattern quen thuộc2x round-trip; giữ lock lâu hơnChậm hơn; cùng độ đúng
Mutex cấp ứng dụng (Redis lock)Cross-DBRủi ro stale lock, single-point-of-failureMong 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)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.