28 июл. 2026 г.

Real-time, очереди и документы: как я проектирую долгие backend-процессы

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

Многие backend-сценарии выглядят простыми только в интерфейсе. Пользователь нажимает кнопку: загрузить Excel, сгенерировать отчет, завершить аукцион, отправить документ на подпись, открыть спор, разослать уведомления.

Если попытаться сделать все это одним HTTP-запросом, система быстро становится неприятной: пользователь ждет, запрос может оборваться, ошибки трудно повторить, а backend начинает смешивать бизнес-действие, тяжелую обработку и уведомления в одном месте.

Я стараюсь проектировать такие процессы как цепочку: команда, состояние, очередь, обработчик, событие, обратная связь.

Синхронно только главное

Первый вопрос, который я задаю: что пользователь должен узнать сразу?

Обычно ему не нужно ждать, пока PDF сконвертируется, письма отправятся, файл пройдет обработку, а все участники получат push. Ему нужно понять, что команда принята и система перешла в ожидаемое состояние.

Поэтому синхронная часть должна быть короткой:

const report = await this.reports.createDraft(userId, input);
await this.queue.add('generate-report', { reportId: report.id });
return report;

Пользователь получает статус PROCESSING, а тяжелая работа уходит в очередь. Это честнее для интерфейса и спокойнее для backend.

Очередь - не мусорный бак

Очереди часто начинают использовать как место, куда можно бросить “что-нибудь тяжелое”. Это работает до первой серьезной ошибки.

Хороший job должен быть понятным:

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

Плохой job знает слишком много о UI, endpoint'е и случайных деталях текущей реализации. Его трудно перезапустить и почти невозможно объяснить в логах.

Статусы важнее прогресс-бара

Не всегда нужно показывать пользователю точный процент выполнения. Но почти всегда нужно показывать понятное состояние:

  • QUEUED - задача поставлена в очередь;
  • PROCESSING - обработка идет;
  • READY - результат готов;
  • FAILED - задача завершилась ошибкой;
  • CANCELLED - процесс отменен.

Такие статусы помогают и frontend'у, и backend'у. UI может обновлять данные, показывать кнопки и подсказки. Backend получает явную модель процесса, а не набор флагов вроде isGenerated, hasError, isSent.

Real-time как обратная связь

WebSocket нужен не ради эффекта. Он нужен там, где пользователь должен увидеть изменение состояния без ручного refresh:

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

Но real-time не должен быть единственным источником истины. Хороший frontend после события умеет инвалидировать данные и перечитать актуальное состояние через API. Событие сообщает: “что-то изменилось”, а API подтверждает: “вот текущее состояние”.

Документы и файлы

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

  • метаданные файла в базе;
  • бинарное содержимое в S3/MinIO;
  • версии файла как отдельную историю;
  • связи файла с бизнес-сущностью;
  • фоновые операции вроде конвертации;
  • права на просмотр и скачивание.

Это особенно важно, если появляются цепочки согласования, подписи, юридические шаблоны или разные роли пользователей. Файл перестает быть “приложением к форме” и становится частью бизнес-процесса.

Ошибки тоже часть UX

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

Если ошибка остается только в логах, пользователь видит вечный PROCESSING. Поэтому я стараюсь проектировать явный failure path: статус, технический лог, пользовательское сообщение и возможность повторить действие там, где это безопасно.

Вывод

Долгие backend-процессы лучше проектировать как маленькую распределенную систему внутри продукта. Не один огромный запрос, а набор понятных частей: команда, статус, очередь, обработчик, событие и обновление данных.

Такой подход не делает систему проще на старте, но делает ее предсказуемее в эксплуатации. А предсказуемость - одна из главных ценностей backend-разработки.

Комментарии

Пока нет комментариев — будьте первым.