Материал учебного циклаОткрытый материал

State Machines: жизненный цикл сущностей

Практическое введение в State Machine и моделирование жизненного цикла сущностей. Разбираем состояния, переходы, события, guard conditions, побочные эффекты и связь State Machine с BPMN, базой данных и API.

Продукт и интерфейсы #State Machine #архитектура #конечный автомат #системный анализ
Учебный цикл Моделирование и проектирование информационных систем Материал 4 из 9

State Machine простыми словами: состояния и переходы объекта

Во многих информационных системах объект существует не просто как набор данных. В течение своей жизни он проходит через несколько состояний.

Например, заявка может быть создана, отправлена, передана на проверку, согласована или отклонена.

Простейший жизненный цикл можно представить так:

Draft -> Submitted -> UnderReview -> Approved

Но одного списка статусов недостаточно.

Нужно понимать:

  • из какого состояния разрешён переход;
  • какое событие запускает переход;
  • какие условия должны выполняться;
  • что произойдёт после изменения состояния;
  • какие переходы запрещены;
  • как хранить историю изменений;
  • кто имеет право выполнить переход.

Для формального описания такого поведения используется State Machine, или конечный автомат.

State Machine позволяет описать жизненный цикл объекта через набор состояний и правила перехода между ними.

Что такое State Machine

State Machine описывает систему через несколько основных понятий:

State - текущее состояние объекта
Transition - переход между состояниями
Event - событие, которое вызывает переход
Guard - условие, разрешающее переход
Action - действие, связанное с переходом

Например:

Draft -> Submit -> Submitted -> StartReview -> UnderReview -> Approve -> Approved

Здесь:

Draft - State
Submit - Event
Draft -> Submitted - Transition

Можно читать это так:

Если заявка находится в состоянии Draft и происходит событие Submit, она переходит в состояние Submitted.

После этого событие StartReview переводит её в UnderReview, а Approve переводит в Approved.

State Machine описывает не просто список возможных статусов, а правила изменения состояния объекта.

Это принципиальное различие.

State и status

В обычном коде состояние часто хранится в поле:

status

Например:

status = "draft"
status = "submitted"
status = "under_review"
status = "approved"

Поэтому в прикладной разработке слова state и status часто используются почти как синонимы.

Но концептуально State Machine шире одного поля status.

Состояние определяет:

  • что сейчас происходит с объектом;
  • какие операции над ним разрешены;
  • какие переходы доступны;
  • какие события могут быть обработаны.

Например, если заявка уже находится в Approved, операция:

Approve application

скорее всего, больше недоступна.

А если заявка находится в Draft, пользователь может:

редактировать -> прикреплять документы -> отправить

После перехода в Submitted часть этих действий может стать недоступной.

Состояние начинает влиять на поведение системы.

Почему списка статусов недостаточно

Простейшая реализация может выглядеть так:

status = any string

Или немного лучше:

status IN (
    "draft",
    "submitted",
    "under_review",
    "approved",
    "rejected"
)

Второй вариант уже ограничивает набор значений, но всё ещё не отвечает на вопрос:

Какие переходы между этими значениями разрешены?

Например:

Draft -> Approved

технически может оказаться возможным, хотя по бизнес-процессу заявка сначала должна быть отправлена и проверена.

Или:

Rejected -> Approved

может быть запрещено полностью.

В таком случае возврат отклонённой заявки должен происходить через отдельный процесс:

Rejected -> Reopened -> Submitted -> UnderReview -> Approved

Хорошая модель определяет не только:

разрешённые состояния

но и:

разрешённые переходы между ними

Поэтому State Machine можно представить как граф:

State -> разрешённый Transition -> State

Не каждая вершина этого графа соединена со всеми остальными.

Transition

Transition - переход объекта из одного состояния в другое.

Например:

Draft -> Submitted
Submitted -> UnderReview
UnderReview -> Approved
UnderReview -> Rejected

Можно сразу заметить, что:

Draft -> Rejected

отсутствует.

Это означает, что такой переход запрещён.

В коде разрешённые переходы можно представить явно:

TRANSITIONS = {
    "draft": {"submitted"},
    "submitted": {"under_review"},
    "under_review": {"approved", "rejected"},
    "approved": set(),
    "rejected": set(),
}

Перед изменением состояния система проверяет:

current_state -> target_state

и убеждается, что такой переход существует.

Это значительно безопаснее, чем разрешить любой части приложения напрямую записывать новое значение в status.

Event

Переход обычно происходит не сам по себе.

Его вызывает Event - событие.

Например:

Submit -> Draft -> Submitted
StartReview -> Submitted -> UnderReview
Approve -> UnderReview -> Approved
Reject -> UnderReview -> Rejected

Здесь состояния описывают положение объекта, а события описывают то, что произошло.

Хорошее разделение:

State - Submitted
Event - StartReview

Не стоит называть состояние:

Submitting
Approve
Create

если на самом деле это действия.

Состояние обычно отвечает на вопрос:

В каком положении сейчас находится объект?

Событие отвечает:

Что произошло?

Например:

State: UnderReview
Event: Approve
Next State: Approved

Guard Conditions

Одного правильного текущего состояния иногда недостаточно.

Представим:

UnderReview -> Approve -> Approved

Но согласовать заявку можно только после проверки документов.

Тогда появляется Guard.

Guard - условие, которое должно выполняться, чтобы transition был разрешён.

Например:

UnderReview -> Approve [documents_verified == true] -> Approved

Если:

documents_verified == false

событие Approve не должно переводить объект в Approved.

Guard может проверять:

  • наличие документов;
  • права пользователя;
  • сумму платежа;
  • заполненность обязательных полей;
  • результаты проверки;
  • наличие связанной сущности;
  • выполнение предыдущего шага.

Например:

Draft -> Submit [required_fields_filled == true] -> Submitted

или:

UnderReview -> Approve [actor.role == manager] -> Approved

При этом важно не превращать Guard в огромный скрытый бизнес-процесс.

Guard должен отвечать на конкретный вопрос:

Разрешён ли сейчас этот переход?

Action

Переход может сопровождаться действием.

Например:

UnderReview -> Approve -> Approved

может дополнительно требовать:

записать дату согласования

или:

сохранить пользователя, который выполнил переход

Это можно рассматривать как Action, связанную с transition.

Например:

Approve
Guard: documents_verified == true
Transition: UnderReview -> Approved
Action: approved_at = now()

Но здесь появляется важная граница между изменением состояния и внешними побочными эффектами.

Side Effects

После перехода система часто должна выполнить дополнительные действия.

Например:

UnderReview -> Approved

может привести к:

  • записи в audit log;
  • отправке email;
  • созданию Celery task;
  • отправке webhook;
  • публикации события в RabbitMQ;
  • обновлению другой сущности;
  • созданию документа.

Это side effects - побочные эффекты перехода.

Их полезно отделять от самого state transition.

Основная логика:

проверить transition -> изменить состояние -> сохранить изменение -> запустить необходимые side effects

Почему это важно?

Представим:

отправить email -> затем сохранить status = approved

Email успешно ушёл, но транзакция базы данных завершилась ошибкой.

Пользователь получил сообщение:

Ваша заявка согласована.

А в базе:

status = under_review

Система оказалась в противоречивом состоянии.

Поэтому изменение критичных данных обычно должно надёжно фиксироваться до выполнения внешних действий.

Для сложных асинхронных систем могут использоваться механизмы вроде Transactional Outbox:

изменение состояния + запись события в одной транзакции -> последующая доставка события

Но сама State Machine не определяет конкретный механизм доставки. Она описывает правила жизненного цикла объекта.

Начальное состояние

У State Machine обычно есть точка входа.

Например:

Initial -> Draft

После создания заявка автоматически получает:

status = Draft

Дальше переходы возможны только из этого состояния.

Например:

Draft -> Submitted -> UnderReview -> Approved

Начальное состояние важно определить явно.

Иначе возникают вопросы:

  • может ли объект создаваться сразу в Submitted;
  • может ли оператор создать Approved запись;
  • всегда ли Draft существует;
  • откуда вообще начинается жизненный цикл.

Конечные состояния

Некоторые состояния завершают жизненный цикл объекта.

Например:

Approved
Rejected
Cancelled

Но не всегда конечное бизнес-состояние означает невозможность дальнейших переходов.

Например:

Rejected -> Reopened

может быть допустимым сценарием.

Поэтому терминальное состояние определяется не названием, а правилами модели.

Если после:

Deleted

никакие переходы невозможны, это действительно terminal state.

Если из Rejected разрешено повторное открытие, то Rejected не является окончательной точкой State Machine.

Один статус не должен заменять несколько независимых признаков

Одна из распространённых ошибок выглядит так:

status =
approved_and_paid_and_email_sent

Здесь в одном статусе смешано сразу несколько независимых характеристик:

application_state
payment_state
notification_state

Получается комбинаторный взрыв.

Например:

approved_not_paid
approved_paid
approved_paid_email_sent
approved_not_paid_email_sent
rejected_not_paid
...

Количество состояний начинает быстро расти.

Если признаки действительно независимы, лучше моделировать их отдельно.

Например:

Application.status = approved
Payment.status = paid
Notification.status = delivered

У каждой сущности может быть собственная State Machine.

Это значительно проще, чем пытаться описать состояние всей системы одной строкой.

State Machine и BPMN

State Machine и BPMN действительно могут выглядеть похожими, потому что обе модели содержат переходы.

Но они отвечают на разные вопросы.

BPMN показывает процесс.

Например:

Получить заявку -> Проверить -> Согласовать -> Уведомить пользователя

State Machine показывает жизненный цикл конкретного объекта.

Например:

Draft -> Submitted -> UnderReview -> Approved

Главное различие можно представить так:

BPMN - что происходит в процессе
State Machine - в каком состоянии находится объект и куда он может перейти

Один BPMN-процесс может одновременно изменять несколько сущностей.

Например:

Application: UnderReview -> Approved
User: Applicant -> Student
Document: Generated -> Sent

Для каждой из них может существовать собственная State Machine.

Как BPMN и State Machine работают вместе

Представим BPMN-фрагмент:

Проверить заявку -> Данные корректны? -> Согласовать заявку

State Machine той же Application может показывать:

Submitted -> UnderReview -> Approved

BPMN объясняет, почему и в каком процессе происходит изменение.

State Machine объясняет, допустимо ли это изменение для конкретной сущности.

Эти модели не заменяют друг друга.

Они описывают одну систему с разных сторон.

State Machine и база данных

Самый простой вариант хранения:

Application

id
status
...

Например:

CREATE TABLE application (
    id BIGSERIAL PRIMARY KEY,
    status TEXT NOT NULL
);

Но база хранит только текущее состояние.

Если нужно знать историю, можно добавить отдельную таблицу.

Application

id
current_status

и:

ApplicationStatusHistory

id
application_id
old_status
new_status
changed_at
changed_by

Связь:

Application 1:N ApplicationStatusHistory

Теперь можно узнать:

  • текущее состояние;
  • когда оно изменилось;
  • из какого состояния был выполнен переход;
  • кто выполнил операцию.

Например:

Draft -> Submitted -> UnderReview -> Approved

может оставить историю:

Draft -> Submitted | 10:15 | user_42
Submitted -> UnderReview | 10:20 | operator_7
UnderReview -> Approved | 11:03 | manager_2

Нужно ли хранить transitions отдельной таблицей

Это зависит от системы.

Разрешённые transitions могут быть определены непосредственно в коде:

TRANSITIONS = {
    "draft": {"submitted"},
    "submitted": {"under_review"},
    "under_review": {"approved", "rejected"},
}

Для относительно стабильной бизнес-логики это часто наиболее простой вариант.

Но в системах, где workflow настраивается пользователем, переходы могут храниться как данные:

State
Transition
Workflow
TransitionRule

Например, администратор может сам создавать:

New -> Processing -> Verification -> Completed

Тогда State Machine становится частью конфигурации системы.

Не нужно автоматически переносить transitions в базу только потому, что они существуют концептуально.

Нужно понять, является ли workflow частью программного кода или изменяемой бизнес-конфигурацией.

State Machine и API

State Machine сильно влияет на проектирование API.

Рассмотрим простой endpoint:

PATCH /applications/42

с телом:

{
  "status": "approved"
}

На первый взгляд удобно.

Но теперь клиент API фактически получает возможность самостоятельно выбирать следующее состояние.

Он может попытаться отправить:

{
  "status": "approved"
}

для заявки в состоянии:

Draft

или даже:

{
  "status": "something"
}

если сервер плохо проверяет значения.

Более явный вариант:

POST /applications/42/approve

Теперь клиент говорит не:

Поставь status = approved.

а:

Выполни бизнес-операцию approve.

Backend уже проверяет:

текущее состояние -> права -> guard conditions -> transition

и только после этого меняет состояние.

Аналогично:

POST /applications/42/submit
POST /applications/42/start-review
POST /applications/42/approve
POST /applications/42/reject

Это command-oriented подход.

Он особенно удобен, если переход содержит сложную бизнес-логику.

Значит ли это, что PATCH использовать нельзя

Нет.

PATCH сам по себе не является проблемой.

Можно реализовать:

PATCH /applications/42

{
  "status": "approved"
}

и внутри backend выполнить полноценную проверку transition.

Проблема возникает, когда API воспринимает status как обычное свободно изменяемое поле.

Например:

application.status = request.data["status"]
application.save()

без проверки жизненного цикла.

Важно не название HTTP endpoint, а наличие единого механизма переходов.

Однако отдельные команды вроде:

approve
reject
submit
cancel

часто лучше отражают бизнес-смысл операции.

Где должна находиться проверка transition

Проверять переход только во frontend недостаточно.

Frontend может скрыть кнопку:

Approve

если операция недоступна.

Но backend всё равно должен самостоятельно проверить переход.

Правило:

Frontend - помогает пользователю выбрать допустимое действие
Backend - гарантирует допустимость transition

Иначе внешний клиент или прямой HTTP-запрос сможет обойти интерфейс.

Права и State Machine

Разрешённый переход может зависеть не только от состояния, но и от пользователя.

Например:

Draft -> Submitted

может выполнять сам автор заявки.

А:

UnderReview -> Approved

только руководитель.

Получается:

Transition = current state + event + actor + guards

Например:

UnderReview
+
Approve
+
role == manager
+
documents_verified == true
->
Approved

Это уже полноценное бизнес-правило.

State Machine помогает собрать такие правила в одном месте, а не распределять случайные проверки по десяткам контроллеров и сервисов.

Параллельные процессы и State Machine

Не всё удобно выражать одной State Machine.

Представим заказ.

У него одновременно могут существовать:

Order status
Payment status
Delivery status

Плохо создавать состояния:

created_not_paid_not_shipped
created_paid_not_shipped
created_paid_shipped
cancelled_refunded_not_shipped
...

Гораздо проще иметь несколько связанных автоматов:

Order: New -> Confirmed -> Completed
Payment: Pending -> Paid -> Refunded
Delivery: Preparing -> Shipped -> Delivered

После этого бизнес-правила связывают их.

Например:

Order можно завершить только если Payment = Paid и Delivery = Delivered

State Machine должна описывать одну понятную ответственность.

State Machine и события системы

Переход можно использовать как источник доменного события.

Например:

UnderReview -> Approved

порождает:

ApplicationApproved

На это событие могут реагировать другие части системы:

ApplicationApproved -> отправить уведомление
ApplicationApproved -> создать участника
ApplicationApproved -> сформировать документы
ApplicationApproved -> обновить аналитику

Так State Machine хорошо сочетается с event-driven архитектурой.

При этом важно разделять:

Transition - изменение состояния
Domain Event - факт того, что изменение произошло
Side Effect - реакция другой части системы

Например:

Approve command -> UnderReview -> Approved -> ApplicationApproved -> send email

Это намного понятнее, чем одна большая функция approve_application(), внутри которой одновременно изменяется база, отправляются письма, вызываются API и создаются документы без явного разделения ответственности.

Идемпотентность переходов

В распределённых системах одно событие иногда может быть доставлено повторно.

Например, команда:

Approve application

пришла два раза.

Если объект уже находится в:

Approved

система должна понимать, что делать.

Варианты зависят от бизнес-логики:

повторный Approve -> вернуть текущее состояние

или:

повторный Approve -> ошибка invalid transition

Главное, чтобы поведение было определено явно.

Особенно важно это для:

  • webhook;
  • message queues;
  • retry;
  • асинхронных задач;
  • сетевых запросов.

Как спроектировать State Machine с нуля

Начинать лучше не с рисования стрелок, а с жизненного цикла сущности.

Представим Application.

Сначала определяем основные состояния:

Draft
Submitted
UnderReview
Approved
Rejected

Затем события:

Submit
StartReview
Approve
Reject

После этого строим переходы:

Draft -> Submit -> Submitted
Submitted -> StartReview -> UnderReview
UnderReview -> Approve -> Approved
UnderReview -> Reject -> Rejected

Затем добавляем guards:

Draft -> Submit [required_fields_filled] -> Submitted
UnderReview -> Approve [documents_verified] -> Approved

После этого определяем права:

Submit -> applicant
StartReview -> operator
Approve -> manager
Reject -> manager

И только затем side effects:

Submitted -> уведомить оператора
Approved -> создать участника + отправить письмо
Rejected -> уведомить заявителя

В итоге проектирование идёт так:

Сущность -> состояния -> события -> переходы -> guards -> права -> side effects -> хранение истории -> API

Типичные ошибки

Первая ошибка - хранить status как произвольную строку.

status = any string

Система перестаёт контролировать множество состояний.

Вторая ошибка - определить допустимые статусы, но не определить transitions.

Draft
Submitted
Approved
Rejected

ещё не являются State Machine.

Нужно знать:

кто -> откуда -> куда -> при каком событии -> при каких условиях

Третья ошибка - использовать один status для нескольких независимых процессов.

Например:

approved_paid_email_sent

вместо отдельных моделей состояния.

Четвёртая ошибка - создавать слишком много состояний.

Не каждое техническое действие является новым State.

Например:

LoadingUserData
CallingDatabase
SerializingResponse

обычно не являются бизнес-состояниями Application.

Пятая ошибка - называть State действием.

Плохо:

Approve
Send
Check

Лучше:

Approved
Submitted
UnderReview

Шестая ошибка - менять status напрямую из разных частей приложения.

Например:

application.status = "approved"

в пяти разных сервисах.

Лучше иметь один механизм transition, который проверяет правила.

Седьмая ошибка - выполнять внешние side effects до фиксации критичного изменения данных.

Сначала система должна надёжно зафиксировать новое состояние, затем организовать выполнение зависимых операций.

Восьмая ошибка - не хранить историю там, где она важна.

Если бизнесу необходимо знать:

кто изменил статус
когда
откуда
куда

одного current_status недостаточно.

Как проверить State Machine

Перед завершением модели полезно проверить несколько вопросов:

  • Определено ли начальное состояние?
  • Понятно ли значение каждого State?
  • Названы ли состояния как состояния, а события как действия?
  • Для каждого ли перехода существует понятное событие?
  • Есть ли запрещённые переходы?
  • Определены ли Guard Conditions?
  • Понятно ли, кто имеет право выполнить transition?
  • Отделены ли side effects от изменения State?
  • Нужна ли история состояний?
  • Не объединены ли несколько независимых процессов одним status?
  • Что произойдёт при повторном событии?
  • Можно ли выполнить переход в обход основного API?
  • Соответствует ли State Machine реальному бизнес-процессу?

Если ответы определены явно, поведение объекта становится намного предсказуемее.

Что важно запомнить

State Machine описывает жизненный цикл одного объекта.

Основные понятия:

State - текущее состояние
Event - произошедшее действие или событие
Transition - разрешённое изменение состояния
Guard - условие перехода
Action - действие, связанное с переходом

Главная идея:

не любой State -> любой State

а:

Current State + Event + Guards -> разрешённый Transition -> New State

Например:

UnderReview + Approve + documents_verified == true -> Approved

State Machine тесно связана с другими моделями системы:

BPMN - показывает процесс
State Machine - жизненный цикл сущности
ERD - хранение текущего состояния и истории
API - команды, вызывающие transitions
Event-driven architecture - реакции на произошедшие переходы

Поэтому State Machine особенно полезна там, где у объекта есть выраженный жизненный цикл: заявки, заказы, платежи, документы, сделки, задачи и другие сущности со статусами.

Главная цель State Machine не в том, чтобы нарисовать много кружков и стрелок.

Хорошая модель должна позволить посмотреть на объект и однозначно понять:

где он находится сейчас -> какие действия доступны -> при каких условиях -> куда он может перейти -> что произойдёт после перехода

После этого статус перестаёт быть случайной строкой в базе и становится формальной частью бизнес-логики системы.

Продолжить

  1. 01 Как подключить внешний API к программе: методы, параметры, заголовки и тело запроса Разбираем, как подключить внешний API к Python-программе или автоматизации: найти endpoint, выбрать HTTP-метод, передать параметры, заголовки и JSON, прочитать ответ и обработать ошибки.
  2. 02 FastAPI: как устроен современный Python API Подробное введение в FastAPI: ASGI, Uvicorn, маршруты, параметры, Pydantic, валидация, зависимости, async, ошибки, middleware, фоновые задачи, тестирование и структура проекта.
  3. 03 PostgreSQL: как устроена реляционная база данных Введение в PostgreSQL: сервер и клиент, таблицы, ключи, связи, SQL, ограничения, JOIN, транзакции, индексы, JSONB, роли, резервные копии и подключение из FastAPI.
  4. 04 Redis: как работает быстрое хранилище данных в памяти Введение в Redis: ключи, структуры данных, TTL, кэширование, eviction, транзакции, Pub/Sub, Streams, persistence, репликация и подключение к FastAPI.
  5. 05 BPMN: от бизнес-процесса до требований к системе Что такое BPMN, зачем моделировать бизнес-процессы и как читать простую диаграмму от начала до результата.
  6. 06 UML: как описывать программную систему с разных сторон Введение в UML и основные виды диаграмм. Разбираем, зачем одной системе нужны разные представления, что показывают Use Case, Sequence, Class, Activity, State Machine, Component и Deployment Diagram и как выбрать подходящую диаграмму под задачу.
  7. 07 ERD и моделирование данных Подробное введение в ERD и моделирование данных. Разбираем сущности, атрибуты, первичные и внешние ключи, связи 1:1, 1:N и M:N, обязательность связей, нормализацию и переход от ERD к PostgreSQL и Django ORM.
  8. 08 DFD: как данные проходят через систему Практическое введение в DFD и моделирование потоков данных. Разбираем внешние сущности, процессы, хранилища и потоки данных, Context Diagram, уровни декомпозиции, баланс между уровнями, связь DFD с API, безопасностью, BPMN и Sequence Diagram.
  9. 09 C4 Model: архитектура программной системы Практическое введение в C4 Model для описания архитектуры информационных систем. Разбираем уровни System Context, Container и Component, дополнительные Dynamic и Deployment Diagram, границы между ними и применение C4 для монолитов и микросервисов.
  10. 10 API Contracts: как системы договариваются о взаимодействии Практическое руководство по проектированию API-контрактов. Разбираем структуру HTTP API, ресурсы и action endpoints, request и response schemas, ошибки, OpenAPI, версионирование, совместимость, аутентификацию, авторизацию, webhook-контракты и безопасное изменение API для существующих consumers.
  11. 11 Event-Driven Modeling: события, очереди и асинхронные процессы Практическое руководство по Event-Driven Modeling и проектированию асинхронных взаимодействий. Разбираем команды и события, Producer, Broker и Consumer, очереди и routing, идемпотентность, порядок событий, retries, Dead Letter Queue, Transactional Outbox, eventual consistency и связь событий с BPMN и State Machine.
  12. 12 Как собрать BPMN, UML, ERD, C4 и API в одну проектную документацию Практическое руководство по сборке проектной документации информационной системы. Показываем, как связать BPMN, UML, ERD, State Machine, DFD, C4, API Contracts и Event-Driven Modeling так, чтобы каждая модель отвечала на свой вопрос и не дублировала остальные.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →

Начните вводить — найдём материал по всей библиотеке.