Событийно‑ориентированные рабочие процессы против запрос‑ответных API для долгих задач
Сравнение событийно‑ориентированных рабочих процессов и запрос‑ответ API для долгих процессов — согласования, таймеры, повторные попытки и аудит в бизнес‑приложениях.

Почему долгие процессы сложны в бизнес‑приложениях
Процесс считается «долгим», когда он не завершается в одном быстром шаге. Он может занимать минуты, часы или дни, потому что зависит от людей, времени или внешних систем. Всё, что включает согласования, передачи ответственности и ожидание, попадает в эту категорию.
Именно здесь простая модель «запрос‑ответ» начинает давать сбои. API‑вызов рассчитан на короткий обмен: отправил запрос, получил ответ, пошёл дальше. Долгие задачи больше похожи на историю с главами. Нужно уметь приостанавливать, точно помнить, на каком вы шаге, и продолжать позже без догадок.
Это видно в повседневных бизнес‑приложениях: согласования покупок, где участвуют менеджер и финансы; адаптация сотрудника, ожидающая проверки документов; возвраты, зависящие от платёжного провайдера; или запросы доступа, которые сначала проверяют, а потом применяют.
Когда команды пытаются трактовать длинный процесс как единый API‑вызов, возникают предсказуемые проблемы:
- Приложение теряет состояние после рестарта или деплоя и не может надёжно возобновить работу.
- Повторные попытки создают дубликаты: второй платёж, второе письмо, двойное согласование.
- Ответственность расплывается: непонятно, кто должен действовать дальше — запросивший, менеджер или системная задача.
- Служба поддержки лишена видимости и не может ответить «где это застряло?» без копания в логах.
- Логика ожидания (таймеры, напоминания, дедлайны) превращается в хрупкие фоновые скрипты.
Конкретный сценарий: сотрудник просит доступ к ПО. Менеджер одобряет быстро, но it‑отделу нужно два дня на предоставление доступа. Если приложение не умеет хранить состояние процесса, отправлять напоминания и безопасно возобновлять работу, получаются ручные напоминания, недовольные пользователи и лишняя работа.
Именно поэтому выбор между событийно‑ориентированными рабочими процессами и запрос‑ответ API имеет значение для долгих бизнес‑процессов.
Две ментальные модели: синхронные вызовы против событий во времени
Самое простое различие сводится к одному вопросу: заканчивается ли работа пока пользователь ждёт, или она продолжается после того, как он ушёл?
Запрос‑ответ — это один обмен: один вызов, один ответ. Он подходит для работы, которая завершается быстро и предсказуемо: создание записи, расчёт цены, проверка наличия. Сервер выполняет работу, возвращает успех или ошибку, и взаимодействие заканчивается.
Событийно‑ориентированный рабочий процесс — это цепочка реакций во времени. Что‑то произошло (заказ создан, менеджер одобрил, таймер сработал), и процесс двигается к следующему шагу. Такая модель подходит для работы с передачами, ожиданиями, повторными попытками и напоминаниями.
Практическая разница — состояние.
В модели запрос‑ответ состояние часто живёт в текущем запросе и в памяти сервера до отправки ответа. В событийных рабочих процессах состояние нужно сохранять (например, в PostgreSQL), чтобы процесс мог возобновиться позже.
И обработка ошибок меняется. В запрос‑ответ обычно при ошибке возвращают ошибку и просят клиента попробовать снова. В рабочих процессах фиксируют неудачу и могут безопасно повторить действие, когда условия улучшатся. Они также могут логировать каждый шаг как событие, что упрощает восстановление хронологии.
Простой пример: «Отправить отчёт о расходах» может быть синхронным. «Получить согласование, ждать 3 дня, напомнить менеджеру, затем выплатить» — уже нет.
Согласования: как каждый подход обрабатывает человеческие решения
Согласования — это то место, где длинная работа становится реальной. Системный шаг может завершиться за миллисекунды, а человек ответить через две минуты или два дня. Ключевой дизайн‑вопрос: моделируете ли вы это ожидание как приостановленный процесс или как новое сообщение, которое приходит позже.
В модели запрос‑ответ согласования часто принимают неудобную форму:
- Блокировка (непрактично)
- Опрос (client спрашивает «одобрено?» снова и снова)
- Коллбэки/вебхуки (сервер звонит вам позже)
Все эти варианты работают, но добавляют инфраструктуру, чтобы связать «человеческое время» с «API‑временем».
В событийной модели согласование читается как последовательность фактов. Приложение записывает «ExpenseSubmitted», а позже приходит «ExpenseApproved» или «ExpenseRejected». Движок рабочего процесса (или ваша собственная машина состояний) переводит запись дальше только после получения следующего события. Это соответствует тому, как люди обычно мыслят о бизнес‑шаге: отправить, проверить, принять решение.
Сложность быстро растёт при множестве согласующих и правилах эскалации. Может потребоваться и менеджер, и финансы, но старший менеджер может переопределить решение. Если такие правила не смоделировать явно, процесс становится трудным для понимания и ещё труднее для аудита.
Простейшая модель согласований, которая масштабируется
Практичный паттерн — держать одну запись‑запрос, а решения хранить отдельно. Так можно поддерживать множество согласующих, не переписывая основную логику.
Зафиксируйте несколько сущностей как первоклассные записи:
- Запрос на согласование: что согласуется и текущий статус
- Индивидуальные решения: кто решил, утвердил/отклонил, временная метка, причина
- Требуемые согласующие: роль или человек и правила порядка
- Правила исхода: «любой», «большинство», «все обязательные», «доступно переопределение»
Какую бы реализацию вы ни выбрали, всегда храните, кто и когда принял решение и почему, как данные, а не как строку лога.
Таймеры и ожидание: напоминания, дедлайны и эскалации
Ожидание — то место, где долгие задачи начинают выглядеть грязно. Люди уходят на обед, календари заполняются, и «мы сообщим» превращается в «кто теперь отвечает?» Это одно из самых явных различий между событийными рабочими процессами и запрос‑ответ API.
В запрос‑ответах время неудобно. HTTP‑вызовы имеют тайм‑ауты, поэтому нельзя держать запрос открытым два дня. Команды обычно приходят к паттернам типа опроса, отдельной плановой задачи, сканирующей базу, или ручных скриптов для просроченных записей. Это работает, но логика ожидания живёт вне процесса. Лёгко пропустить граничные случаи: что если задача запустится дважды или запись изменилась прямо перед отправкой напоминания?
Рабочие процессы рассматривают время как обычный шаг. Можно задать: подождать 24 часа, отправить напоминание, затем подождать до 48 часов и эскалировать на другого согласующего. Система хранит состояние, поэтому дедлайны не прячутся в отдельном cron‑проекте.
Простое правило согласования можно описать так:
После подачи отчёта о расходах ждать 1 день. Если статус по‑прежнему «Pending», отправить сообщение менеджеру. Через 2 дня, если всё ещё «Pending», переназначить дело на руководителя менеджера и записать эскалацию.
Ключевой момент — что делать, когда таймер срабатывает, а мир уже изменился. Хороший рабочий процесс всегда перепроверяет текущее состояние перед действием:
- Загружает последний статус
- Подтверждает, что он всё ещё «Pending»
- Подтверждает, что назначенный действующий (assignee) всё ещё валиден (команды меняются)
- Записывает, что было сделано и почему
Повторные попытки и восстановление без дублирования действий
Повторные попытки нужны, когда что‑то упало по причинам вне вашего полного контроля: платёжный шлюз тайм‑аутит, почтовый сервис вернул временную ошибку, или приложение сохранило шаг A, но упало до шага B. Опасность проста: вы пробуете снова и случайно выполняете действие дважды.
В запрос‑ответ модели клиент вызывает endpoint, ждёт, и если не получает явного успеха, пробует снова. Чтобы это было безопасно, сервер должен считать повторные вызовы тем же намерением.
Практичное решение — ключ идемпотентности: клиент отправляет уникальный токен вроде pay:invoice-583:attempt-1. Сервер сохраняет результат для этого ключа и возвращает тот же результат при повторах. Это предотвращает двойные списания, дублирование заявок или повторные согласования.
У событийных рабочих процессов другой тип риска дублирования. События часто доставляются «как минимум один раз», то есть дубликаты могут появляться даже при корректной работе. Потребители должны делать дедупликацию: сохранять event ID (или бизнес‑ключ вроде invoice_id + step) и игнорировать повторы. Это ключевое различие: запрос‑ответ фокусируется на безопасном повторном выполнении вызовов, события — на безопасном повторном проигрывании сообщений.
Несколько правил повторных попыток работают в обеих моделях:
- Используйте backoff (например: 10 с, 30 с, 2 м).
- Установите лимит попыток.
- Разделяйте временные ошибки (повторять) и постоянные ошибки (быстро проваливать).
- Переводите повторные неудачи в состояние «требуется внимание».
- Логируйте каждую попытку, чтобы потом объяснить, что случилось.
Повторные попытки должны быть явными в процессе, а не скрытой логикой. Так вы делаете ошибки видимыми и устранимыми.
Аудит: как сделать процесс объяснимым
Аудит — это ваш файл «почему». Когда спросят «почему этот отчёт отклонили?», вы должны уметь ответить без домыслов, даже через месяцы. Это важно и для событийных рабочих процессов, и для запрос‑ответ моделей, но выглядит по‑разному.
Для любого долгого процесса записывайте факты, которые позволят воспроизвести историю:
- Actor: кто совершил действие (пользователь, сервис или системный таймер)
- Time: когда это произошло (с часовым поясом)
- Input: что было известно в тот момент (сумма, поставщик, пороговые политики, согласования)
- Output: какое решение или действие было выполнено (утверждено, отклонено, оплачено, повторено)
- Версия правила: какая версия политики/логики использовалась
Событийные рабочие процессы упрощают аудит, потому что каждый шаг естественно порождает событие вроде «ManagerApproved» или «PaymentFailed». Если вы храните эти события с payload и actor, получаете чистую хронологию. Важно делать события описательными и хранить их там, где можно запросить по делу.
В модели запрос‑ответ аудит возможен, но история часто разбросана по сервисам. Один endpoint пишет «approved», другой — «payment requested», третий — «retry succeeded». Если форматы разные, аудит превращается в детективную работу.
Простое исправление — общий case ID (корреляционный ID). Это идентификатор, который вы прикрепляете ко всем запросам, событиям и записям БД для экземпляра процесса, например «EXP-2026-00173». Тогда вы можете проследить весь путь.
Как выбрать подход: сильные стороны и компромиссы
Лучший выбор зависит от того, нужен ли ответ прямо сейчас или процесс должен продолжаться часы или дни.
Запрос‑ответ хорош, когда работа короткая и правила простые. Пользователь отправляет форму, сервер валидирует, сохраняет данные и возвращает успех или ошибку. Это также подходит для одноступенчатых действий: create, update, check permissions.
Он начинает плохо справляться, когда «один запрос» тайно превращается в множество шагов: ожидание согласования, вызов множества внешних систем, обработка тайм‑аутов или ветвление в зависимости от дальнейших событий. Либо вы держите соединение открытым (хрупко), либо вы выносите ожидания и повторные попытки в фоновые задания, которые сложно анализировать.
Событийно‑ориентированные рабочие процессы особенно хороши, когда процесс — это история во времени. Каждый шаг реагирует на новое событие (approved, rejected, timer fired, payment failed) и решает, что делать дальше. Это упрощает паузу, возобновление, повторные попытки и оставляет ясный след причин действий.
Есть реальные компромиссы:
- Простота против надёжности: запрос‑ответ проще начать, событийные процессы надёжнее при больших задержках.
- Стиль отладки: запрос‑ответ идёт по прямой линии, рабочие процессы требуют трассировки по шагам.
- Инструменты и практики: события требуют хорошего логирования, корреляционных ID и ясных моделей состояния.
- Управление изменениями: рабочие процессы ветвятся и эволюционируют; событийные модели легче поддерживают новые пути, если их правильно смоделировать.
Практический пример: отчёт о расходах, требующий согласования менеджера, затем проверки финансов, затем выплаты. Если платёж падает, вы хотите повторные попытки без двойной оплаты — это естественно событийная задача. Если же это просто «отправить отчёт» с быстрыми проверками, запрос‑ответ обычно достаточно.
Пошагово: как спроектировать долгий процесс, который переживёт задержки
Долгие бизнес‑процессы ломаются банально: вкладка в браузере закрылась, сервер перезапустился, согласование ждёт три дня, платёжный провайдер тайм‑аутит. Проектируйте с учётом этих задержек с самого начала, независимо от выбранной модели.
Начните с определения небольшого набора состояний, которые вы можете сохранять и возобновлять. Если вы не можете указать текущее состояние в базе, у вас нет настоящего возобновляемого процесса.
Простой порядок действий
- Задайте границы: определите триггер старта, условие завершения и несколько ключевых состояний (Ожидание согласования, Утверждено, Отклонено, Истёк, Завершено).
- Дайте имена событиям и решениям: опишите, что может произойти со временем (Submitted, Approved, Rejected, TimerFired, RetryScheduled). Держите имена событий в прошедшем времени.
- Выберите точки ожидания: отметьте, где процесс приостанавливается для человека, внешней системы или дедлайна.
- Добавьте правила таймеров и повторных попыток для каждого шага: решите, что делать при наступлении времени или ошибке вызова (backoff, max attempts, эскалация, отказ).
- Определите, как процесс возобновляется: при каждом событии или коллбэке загружайте сохранённое состояние, проверяйте его валидность и переходите к следующему состоянию.
Чтобы пережить рестарты, сохраняйте минимально необходимое для безопасного продолжения:
- ID экземпляра процесса и текущее состояние
- Кто может действовать дальше (исполнитель/роль) и что он решил
- Дедлайны (due_at, remind_at) и уровень эскалации
- Метаданные повторных попыток (число попыток, последняя ошибка, next_retry_at)
- Идемпотентный ключ или флаги «уже выполнено» для побочных эффектов (отправка сообщения, списание карты)
Если вы можете восстановить «где мы» и «что можно делать дальше» из сохранённых данных, задержки перестают быть страшными.
Типичные ошибки и как их избежать
Долгие процессы обычно ломаются в реальных условиях: согласование занимает два дня, повторная попытка срабатывает не в тот момент, и вы получаете двойную оплату или потерянный аудит.
Частые ошибки:
- Держать HTTP‑запрос открытым в ожидании человеческого согласования. Он тайм‑аутит, зануляет ресурсы сервера и даёт пользователю ложное ощущение, что «что‑то происходит».
- Повторять вызовы без идемпотентности. Сетевой сбой превращается в дублированные счета, письма или повторные переходы в «Approved».
- Не сохранять состояние процесса. Если состояние в памяти — рестарт его сотрёт. Если только в логах — дальше нельзя корректно продолжить.
- Делать аудиторский след «размытым». События имеют разные часы и форматы, и временная шкала не будет надёжной при инциденте или проверке соответствия.
- Смешивать асинхронность и синхронность без единого источника истины. Одна система говорит «Paid», другая — «Pending», и никто не знает, что верно.
Простой пример: отчёт согласовали в чате, вебхук пришёл с опозданием, и API платежей запустил повторную попытку. Без сохранённого состояния и идемпотентности повтор может отправить платёж дважды, и ваши записи не объяснят почему.
Большинство исправлений сводится к явности:
- Сохраняйте переходы состояний (Requested, Approved, Rejected, Paid) в базе с указанием, кто/что их сделал.
- Используйте идемпотентные ключи для внешних побочных эффектов и сохраняйте результат по ключу.
- Разделяйте «принять запрос» и «завершить работу»: возвращайте клиенту быстрый ответ, а остальное выполняйте в фоне.
- Стандартизируйте временные метки (UTC), добавляйте корреляционные ID и записывайте и запрос, и результат.
Короткий чек‑лист перед началом разработки
Долгая работа — это не про один идеальный вызов, а про корректность после задержек, участия людей и сбоев.
Запишите, что для вашего процесса значит «безопасно продолжить». Если приложение рестартует посередине, вы должны уметь продолжить с последнего известного шага без догадок.
Практический чек‑лист:
- Определите, как процесс возобновится после краша или деплоя. Что сохранено, и что выполнится дальше?
- Дайте каждой инстанции уникальный ключ процесса (например, ExpenseRequest-10482) и ясную модель статусов (Submitted, Waiting for Manager, Approved, Paid, Failed).
- Рассматривайте согласования как записи, а не просто как флаги: кто утвердил/отклонил, когда и с каким комментарием.
- Замапьте правила ожидания: напоминания, дедлайны, эскалации, истечения. Назначьте владельца для каждого таймера (менеджер, финансы, система).
- Спланируйте обработку ошибок: повторные попытки ограничены и безопасны, и должен быть стоп‑пункт «требует ревью», где человек поправит данные или инициирует повтор.
Простая проверка здравомыслия: представьте, что платёжный провайдер тайм‑аутит после того, как вы уже списали карту. Дизайн должен предотвращать двойное списание и при этом позволять завершить процесс.
Пример: согласование расходов с дедлайном и повторной попыткой платежа
Сценарий: сотрудник отправляет квитанцию на $120 такси. Нужна согласование менеджера в течение 48 часов. Если согласовано, система выплачивает сотруднику. При ошибке платежа система безопасно повторяет попытку и оставляет ясную запись.
Проходка в модели запрос‑ответ
В запрос‑ответ приложении часто действует разговорная модель: надо постоянно проверять.
Сотрудник нажимает Submit. Сервер создаёт запись reimbursement со статусом «Pending approval» и возвращает ID. Менеджер получает уведомление, а клиент приложения обычно опрашивает статус через «GET reimbursement status by ID».
Чтобы обеспечить 48‑часовой дедлайн, вы либо запускаете плановую задачу, которая сканирует просроченные заявки, либо сохраняете timestamp дедлайна и проверяете его при опросах. Если задача задержится, пользователи увидят устаревший статус.
Когда менеджер одобряет, сервер меняет статус на «Approved» и вызывает платёжного провайдера. Если Stripe вернул временную ошибку, сервер должен решить: повторить сейчас, позже или провалить. Без аккуратных идемпотентных ключей повтор может привести к двойной выплате.
Проходка в событийной модели
В событийной модели каждое изменение — факт.
Сотрудник отправляет отчёт — порождается событие «ExpenseSubmitted». Запускается workflow и ждёт либо «ManagerApproved», либо таймерное событие «DeadlineReached» через 48 часов. Если таймер срабатывает первым, workflow фиксирует «AutoRejected» и причину.
При одобрении workflow записывает «PayoutRequested» и пытается провести платёж. Если Stripe тайм‑аутит, система записывает «PayoutFailed» с кодом ошибки, планирует повторную попытку (например, через 15 минут) и фиксирует «PayoutSucceeded» только один раз, используя идемпотентный ключ.
Что видит пользователь:
- Pending approval (осталось 48 часов)
- Approved, идёт выплата
- Повторная попытка платежа запланирована
- Выплачено
Аудит читается как хронология: submitted, approved, deadline checked, payout attempted, failed, retried, paid.
Следующие шаги: превратить модель в рабочее приложение
Выберите один реальный процесс и реализуйте его полностью, прежде чем обобщать. Согласование расходов, адаптация сотрудников и обработка возвратов хороши для старта — они включают человеческие шаги, ожидание и пути ошибок. Цель маленькая: один «happy path» и две наиболее частые исключительные ситуации.
Опишите процесс как набор состояний и событий, а не как экраны. Например: «Submitted» -> «ManagerApproved» -> «PaymentRequested» -> «Paid», с ветвлениями «ApprovalRejected» или «PaymentFailed». Когда точки ожидания и побочные эффекты явны, выбор между событийной моделью и запрос‑ответом становится практическим.
Определите, где хранится состояние процесса. База данных может быть достаточной, если поток прост и вы сможете централизованно применять обновления. Движок рабочих процессов помогает, когда нужны таймеры, повторные попытки и ветвления, потому что он отслеживает, что должно произойти дальше.
Добавляйте поля аудита с самого начала. Храните, кто что сделал, когда это произошло и почему (комментарий или код причины). Когда спросят «почему этот платёж был повторён?», вы хотите ясный ответ без копания в логах.
Если вы строите рабочие процессы в no‑code платформе, AppMaster (appmaster.io) — один из вариантов, где можно моделировать данные в PostgreSQL и визуально строить логику процесса, что облегчает поддержание согласований и аудита в веб‑ и мобильных приложениях.
Вопросы и ответы
Используйте запрос‑ответ, когда работа завершается быстро и предсказуемо — пока пользователь ждёт: создание записи, валидация формы. Выбирайте событийно‑ориентированный рабочий процесс, когда процесс длится минуты или дни, включает человеческие согласования, таймеры, повторные попытки и необходимость безопасно возобновлять выполнение после перезапуска.
Долгие задачи не вписываются в один HTTP‑запрос: соединения тайм-аутят, сервера рестартуют, а работа часто зависит от людей или внешних систем. Если пытаться поместить весь процесс в один вызов, вы теряете состояние, получаете дубликаты при повторных попытках и вынужденно выносите логику ожидания в разбросанные фоновые скрипты.
Хорошая практика — сохранять в базе явное состояние процесса и продвигать его только через явные переходы. Храните ID экземпляра процесса, текущий статус, кто может действовать дальше и ключевые временные метки, чтобы после деплоя, краша или задержки можно было безопасно продолжить выполнение.
Модель — представлять согласование как паузу, которая возобновляется при поступлении решения, а не держать соединение открытым или постоянно опрашивать. Записывайте каждое решение как данные: кто решил, когда, «утвердил/отклонил» и причина — это позволяет предсказуемо двигать процесс и проводить аудит.
Опрос (polling) может работать в простых случаях, но добавляет нагрузку и задержки — клиент постоянно спрашивает «готово ли?». Лучше отправлять уведомление об изменении состояния и позволять клиенту обновляться по требованию, при этом сервер остаётся единственным источником истины.
Рассматривайте время как часть процесса: сохраняйте дедлайны и времена напоминаний, а затем при срабатывании таймера проверяйте текущее состояние перед действием. Это предотвращает отправку напоминаний после того, как заявка уже была одобрена, и делает эскалации устойчивыми к задержкам или повторным запускам заданий.
Для побочных эффектов (платёж, письмо и т. п.) используйте идемпотентные ключи и сохраняйте результат по этому ключу. Тогда повторный запрос с тем же намерением вернёт прежний результат вместо повторного выполнения действия.
Предполагаем, что сообщения могут доставляться более одного раза, и делаем потребителей устойчивыми к дубликатам. Практический подход — сохранять event ID (или бизнес‑ключ для шага) и игнорировать повторы, чтобы повторная доставка не вызвала повторного действия.
Фиксируйте хронологию фактов: кто (actor), временная метка (с часовым поясом), входные данные на момент события, результат (утверждён, отклонён, выплачен, повторная попытка) и версия правил/политики. Привязывайте всё к одному case‑ID/корреляционному ID, чтобы служба поддержки могла легко найти «где застряло» без рытья по несвязным логам.
Держите одну запись‑запрос как «дело», решения храните отдельно, и делайте переходы состояния через сохранённые транзакции, которые можно воспроизвести. В no‑code инструменте вроде AppMaster вы можете моделировать данные в PostgreSQL и визуально реализовать логику шагов, что помогает сохранить согласования, повторные попытки и поля аудита в едином виде.


