Order state machine design 2026: проектирование state-машины заказов
Order state machine — это сердце любого B2B-фулфилмента. Правильный дизайн делает систему наблюдаемой, отлаживаемой и audit-friendly. Неправильный — порождает impossible states и race conditions, которые ловить можно месяцами. Эта статья — детальный гайд по проектированию FSM для digital-goods order.
1. Шесть базовых состояний
enum OrderState {
PENDING = 'pending', // создан, ждёт payment + reservation
RESERVED = 'reserved', // inventory locked под этот заказ
PROCESSING = 'processing', // supplier API вызван, ждём webhook
DELIVERED = 'delivered', // success, код выдан клиенту
FAILED = 'failed', // terminal — supplier отверг или timeout
REFUNDED = 'refunded', // terminal — деньги вернулись клиенту
}
Допустимые переходы (граф):
pending → reserved | failed
reserved → processing | failed
processing → delivered | failed
delivered → refunded
failed → (terminal)
refunded → (terminal)
Все другие переходы (например, delivered → processing) — invalid и должны бросать ошибку.
2. Инварианты
Инвариант = свойство, всегда истинное независимо от состояния. Для order:
total_amount > 0reserved_at IS NULL⇔state ∈ {pending, failed}delivered_at IS NULL⇔state ≠ deliveredrefund_amount <= total_amountstate = refunded⇒delivered_at IS NOT NULL
Инварианты проверяются ассертами в FSM transition handler и в DB через CHECK constraints:
ALTER TABLE orders ADD CONSTRAINT chk_refund_amount
CHECK (refund_amount <= total_amount);
ALTER TABLE orders ADD CONSTRAINT chk_refunded_delivered
CHECK (state != 'refunded' OR delivered_at IS NOT NULL);
3. XState или enum+switch?
Для small/medium projects — enum + switch достаточно:
async function transition(order: Order, event: OrderEvent): Promise<Order> {
const next = nextState(order.state, event.type);
if (!next) throw new Error(`invalid transition ${order.state} + ${event.type}`);
return db.tx(async (tx) => {
const updated = await tx.orders.update(order.id, {
state: next,
version: order.version + 1, // optimistic lock
}, { where: { version: order.version } });
await tx.audit.insert({
event_id: randomUUID(),
order_id: order.id,
from_state: order.state,
to_state: next,
actor: event.actor,
timestamp: new Date(),
});
return updated;
});
}
Для complex flows (multi-supplier, partial-delivery, fraud-hold) — XState даёт визуальную диаграмму и hierarchical states.
4. Сравнение реализаций
| Approach | Lines of code | Visualizable | Type-safe | Performance |
|---|---|---|---|---|
| Boolean flags | 50 | No | No | Best |
| Enum + switch | 150 | No | Strong | Best |
| XState | 200+ | Yes (Inspector) | Strong | Good |
| Temporal workflow | 500+ | Yes | Strong | Good (async) |
FoxReload во внутреннем core использует enum+switch (хорошо для p95 transition 8ms). Партнёрам рекомендуем XState для UI-визуализации order-lifecycle.
CTA
FoxReload API экспонирует current state и audit log через GET /v1/orders/{id}/events — webhook-event-stream совпадает с внутренним FSM. Получите доступ.
