Как рисовал статусную модель на проекте?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Статусная модель на проекте
Статусная модель (State Machine) — один из самых ценных инструментов BA. Правильно спроектированная модель предотвращает баги и путаницу в разработке. Вот мой практический опыт:
1. Что такое статусная модель?
Это диаграмма, которая показывает:
- Какие состояния может иметь объект (заказ, платёж, документ)
- Какие переходы между состояниями возможны
- При каких условиях происходит переход
- Какие действия выполняются при переходе
Пример: жизненный цикл заказа в интернет-магазине
New → Processing → Shipped → Delivered → Completed
\ ↓ ↓
└─ Cancelled Returned ← Return Requested
2. Практический пример: статусная модель платежа
Мы проектировали систему обработки платежей. Вот как я рисовал модель:
Шаг 1: Определил все возможные статусы
Pending (ожидание)
→ Processing (обработка)
→ Completed (успешно)
→ Failed (ошибка)
→ Cancelled (отменён)
→ Refunded (возвращено)
Шаг 2: Определил правила переходов
Pending:
- Может перейти в Processing (когда пользователь подтверждает платёж)
- Может перейти в Cancelled (если истёк timeout, например 15 минут)
- НЕ может прыгнуть напрямую в Completed
Processing:
- Может перейти в Completed (если платежная система вернула success)
- Может перейти в Failed (если ошибка от платёжки)
- НЕ может вернуться в Pending
Completed:
- Может перейти в Refunded (если клиент попросил refund в течение 30 дней)
- НЕ может перейти в никакие другие статусы
Failed:
- Может перейти в Processing (если пользователь захочет retry)
- Может перейти в Cancelled (если истёк timeout)
- НЕ может перейти в Completed (это была бы ошибка!)
Cancelled и Refunded:
- Финальные состояния, переходов нет
Шаг 3: Визуализация
Рисовал в Figma/Lucidchart:
[Pending]
↙ ↘
timeout confirm
↙ ↘
[Cancelled] [Processing]
↙ ↘
success error
↙ ↘
[Completed] [Failed]
↓ ↓
refund retry → [Processing]
↓ ↓
[Refunded] [Cancelled]
3. Детали при проектировании статусной модели
Вопросы, которые задаю себе и стейкхолдерам:
-
Сколько финальных состояний?
- Платёж: Completed, Failed, Cancelled, Refunded (4 финальных)
- Заказ: Delivered, Cancelled, Returned (3 финальных)
-
Есть ли timeout переходы?
- Платёж 15 минут в Pending → автоматически Cancelled
- Обработка заказа не более 24 часов
-
Какие переходы требуют пользовательского действия?
- Платёж: Pending → Processing (пользователь нажимает "Pay")
- Платёж: Completed → Refunded (пользователь нажимает "Request Refund")
-
Какие переходы системные (автоматические)?
- Processing → Completed (webhook от платежной системы)
- Processing → Failed (webhook с ошибкой)
-
Есть ли условия на переходы?
- Refund только для платежей в Completed статусе
- Refund возможен только в течение 30 дней
- Refund максимум один раз
4. Документирование переходов (State Transition Table)
Создавал таблицу в Confluence/Notion:
| From Status | To Status | Trigger | Condition | Action |
|-------------|------------|---------|-----------|--------|
| Pending | Processing | User confirms | timeout < 15min | Send to payment gateway, log timestamp |
| Pending | Cancelled | Timeout | 15 min elapsed | Send email to user, release order hold |
| Processing | Completed | Webhook: success | Payment gateway response OK | Update balance, send confirmation email |
| Processing | Failed | Webhook: error | Payment gateway returns error | Log error, offer retry, send notification |
| Completed | Refunded | User requests | Within 30 days | Hold funds, send to payment gateway |
| Failed | Processing | User retries | Max 3 retries | Send to payment gateway again |
Это предотвращает баги потому что:
- Разработчик видит, какие переходы возможны
- Нет вопроса "а можно ли из Failed вернуться в Pending?" (нет, таблица ясно это показывает)
- Если требование меняется, мы обновляем таблицу и все видят изменения
5. Граничные случаи (Edge Cases)
Всегда спрашиваю: что если...?
-
"Что если платёж прошёл в платёжной системе, но webhook не пришёл?"
- Нужна reconciliation задача
- Каждый час проверяем Processing платежи старше 5 минут
- Запрашиваем статус у платёжки
- Обновляем статус если нужно
-
"Что если пользователь нажал "Refund" два раза?"
- Блокируем кнопку после первого нажатия (UI)
- На бэке: проверяем, есть ли уже refund для этого платежа
- Если есть → возвращаем существующий, не создаём новый
-
"Что если платёж в Pending, но пользователь закрыл браузер и ушёл?"
- Через 15 минут автоматически Cancelled
- Если вернётся и захочет оплатить → создаём новый платёж
- Старый платёж остаётся в Cancelled для аудита
-
"Что если платёж Failed, но пользователь попытался refund?"
- Блокируем refund для статусов != Completed
- Возвращаем ошибку 400 Bad Request с понятным сообщением
6. Реализация в коде
Пример: как это выглядит в Python/FastAPI
from enum import Enum
class PaymentStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
REFUNDED = "refunded"
# State transitions map
ALLOWED_TRANSITIONS = {
PaymentStatus.PENDING: [
PaymentStatus.PROCESSING,
PaymentStatus.CANCELLED,
],
PaymentStatus.PROCESSING: [
PaymentStatus.COMPLETED,
PaymentStatus.FAILED,
],
PaymentStatus.COMPLETED: [
PaymentStatus.REFUNDED,
],
PaymentStatus.FAILED: [
PaymentStatus.PROCESSING,
PaymentStatus.CANCELLED,
],
PaymentStatus.CANCELLED: [],
PaymentStatus.REFUNDED: [],
}
def can_transition(from_status, to_status):
allowed = ALLOWED_TRANSITIONS.get(from_status, [])
return to_status in allowed
def transition_payment(payment_id, new_status, reason):
payment = db.get_payment(payment_id)
if not can_transition(payment.status, new_status):
raise ValueError(
f"Cannot transition from {payment.status} to {new_status}"
)
# Проверяем условия
if new_status == PaymentStatus.REFUNDED:
if (datetime.now() - payment.created_at).days > 30:
raise ValueError("Refund allowed only within 30 days")
# Выполняем действие
if new_status == PaymentStatus.PROCESSING:
send_to_payment_gateway(payment)
if new_status == PaymentStatus.COMPLETED:
update_user_balance(payment.user_id, payment.amount)
send_confirmation_email(payment.user_email)
if new_status == PaymentStatus.REFUNDED:
request_refund_from_gateway(payment)
# Обновляем в БД
db.update_payment(
payment_id,
status=new_status,
updated_at=datetime.now(),
transition_reason=reason
)
# Логируем
log_state_transition(
entity_type="payment",
entity_id=payment_id,
from_status=payment.status,
to_status=new_status,
reason=reason,
timestamp=datetime.now()
)
7. Тестирование статусной модели
В тестах проверяю все переходы:
def test_payment_transitions():
# Valid transitions
assert can_transition(PENDING, PROCESSING)
assert can_transition(PROCESSING, COMPLETED)
assert can_transition(COMPLETED, REFUNDED)
# Invalid transitions
assert not can_transition(PENDING, COMPLETED) # Нельзя прыгнуть через Processing
assert not can_transition(COMPLETED, FAILED) # Completed финальный
assert not can_transition(CANCELLED, PENDING) # Cancelled финальный
def test_refund_only_within_30_days():
payment = create_payment(created_at=30.days.ago)
with pytest.raises(ValueError):
transition_payment(payment.id, REFUNDED, "user requested")
payment = create_payment(created_at=29.days.ago)
transition_payment(payment.id, REFUNDED, "user requested")
assert payment.status == REFUNDED
def test_cannot_transition_unknown_status():
with pytest.raises(ValueError):
transition_payment(payment_id, "invalid_status", "test")
8. Сложные модели: вложенные статусы
Иногда один статус может содержать подстатусы
Пример: заказ в статусе "Shipped" может иметь подстатусы:
- In Transit
- Out for Delivery
- Delivered
Order Status: Shipped
└─ Shipment Status: In Transit
└─ Location: Moscow region
└─ Estimated Delivery: 2025-03-28
└─ Shipment Status: Out for Delivery
└─ Location: User's building
└─ Estimated Delivery: 2025-03-28
└─ Shipment Status: Delivered
└─ Delivered At: 2025-03-28 14:35:22
└─ Signed By: Recipient
9. Диаграмма vs Таблица
Когда диаграмма:
- Показываем бизнесу в целом (overview)
- Демонстрируем на встречах
- Нужна визуальная ясность
Когда таблица:
- Даём разработчикам (детали)
- Документируем все условия
- Нужны exact specifications
Лучше ОБЕ — диаграмма для бизнеса, таблица для разработчиков.
10. Инструменты которые я использовал
Диаграммы:
- Figma (простой и красивый)
- Lucidchart (больше возможностей)
- Draw.io (бесплатно и онлайн)
- PlantUML (для генерации из кода)
Документация:
- Confluence (таблицы и диаграммы)
- Notion (живой документ с комментариями)
- GitHub wiki (если всё в гите)
11. Ошибки которые я делал в прошлом
-
Рисовал диаграмму без таблицы
- Разработчик не знал, нужна ли проверка условия или нет
- Результат: баг на продакшене
- Вывод: всегда таблица с условиями
-
Забывал про edge cases
- Не подумал: что если платёж в Processing и пользователь нажал "Retry"?
- Результат: дублирование платежей
- Вывод: всегда спрашиваю "что если...?"
-
Слишком много переходов
- Модель становилась сложной
- Результат: сложно тестировать, много багов
- Вывод: нужен баланс — простота vs полнота
-
Не обновлял модель при изменениях
- Требования меняются
- Но диаграмма остаётся старой
- Результат: confusion в команде
- Вывод: живой документ, обновляю сразу
Чеклист для статусной модели
- ✅ Все возможные статусы определены
- ✅ Все переходы задокументированы
- ✅ Граничные случаи обсуждены
- ✅ Timeout переходы определены
- ✅ Условия для каждого перехода ясны
- ✅ Финальные статусы определены
- ✅ Диаграмма нарисована (визуализация)
- ✅ Таблица переходов создана (для разработчиков)
- ✅ Тесты покрывают все переходы
- ✅ Документация обновляется вместе с кодом
Итог
Статусная модель — это основа правильного дизайна. Когда она ясная и полная, разработчики знают точно что кодировать, не бывает неожиданных edge cases, и система работает предсказуемо.