Đưa các chuyển trạng thái hợp lệ thành dữ liệu, thay vì để mỗi handler tự kiểm bằng if.
ts
const TRANSITIONS = {
pending: ['paid', 'cancelled'],
paid: ['shipped', 'refunded'],
shipped: ['completed', 'refunded'],
completed: [],
cancelled: [],
refunded: [],
} as const
function assertTransition(from: Status, to: Status) {
if (!TRANSITIONS[from].includes(to)) {
throw new InvalidTransition(from, to)
}
}Toàn bộ quy tắc nằm ở một chỗ đọc được, và trạng thái kết thúc (completed, cancelled) tự nhiên là danh sách rỗng.
Những phần bắt buộc phải có khi chạy thật:
- Chuyển trạng thái phải nguyên tử ở tầng DB, không chỉ ở tầng ứng dụng. Hai webhook về cùng lúc sẽ vượt qua mọi kiểm tra trong bộ nhớ. Dùng cập nhật có điều kiện:
sql
update orders set status = 'paid'
where id = $1 and status = 'pending';
-- 0 dòng bị ảnh hưởng = ai đó đã chuyển trước, dừng lại- Side effect gắn với chuyển trạng thái, không gắn với "trạng thái hiện tại là X". Trừ kho khi chuyển sang
paid, nếu không webhook lặp sẽ trừ kho hai lần. - Ghi lại lịch sử chuyển trạng thái (from, to, thời điểm, ai/cái gì gây ra) — đây là thứ cần đầu tiên khi đi điều tra đơn lỗi.
- Khi mỗi trạng thái có hành vi khác nhau (không chỉ chuyển tiếp), lúc đó mới nâng lên State pattern: mỗi trạng thái một class với các method nghiệp vụ riêng. Với vòng đời đơn hàng, bảng chuyển trạng thái như trên thường đã đủ.