Оптовая платформа цифровых товаров

Order state machine design 2026: проектирование state-машины заказов

Production-дизайн state-машины B2B-заказа: 6 базовых состояний, инварианты переходов, audit log и реализация на TypeScript.

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:

  1. total_amount > 0
  2. reserved_at IS NULLstate ∈ {pending, failed}
  3. delivered_at IS NULLstate ≠ delivered
  4. refund_amount <= total_amount
  5. state = refundeddelivered_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. Получите доступ.

Часто задаваемые вопросы

Сколько состояний должно быть в order FSM?
Минимум 5–6 для digital goods: pending → reserved → processing → delivered → (refunded). Failed как absorbing terminal state. Slack-состояния типа awaiting_payment или partial_fulfillment добавляйте только если бизнес-логика требует — каждое лишнее состояние — это +5% bugs.
Можно ли использовать boolean-флаги вместо state-машины?
Нет. Flags is_paid, is_shipped, is_refunded создают impossible states (например, refunded=true при is_shipped=false). FSM с enum гарантирует, что каждый order в exactly одном валидном состоянии. Это страхует от 30%+ bugs.
Как откатывать заказ при ошибке?
FSM не «откатывает». Из processing при ошибке поставщика переход в failed (terminal), потом separate transition failed → refunded запускает возврат на баланс. Никаких backward-edges — это нарушает audit-trail.
Что должно быть в audit log каждого transition?
event_id (UUIDv4), order_id, from_state, to_state, timestamp, actor (user_id или 'system'), reason, source_ip, metadata (например, supplier_id для processing→delivered). Append-only, никакие updates на старых записях.
Получить доступ к FoxReload API

Похожие статьи