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-разработки.