← назад к разделу

Каждая связь между сервисами — это выбор: позвать REST-ом и дождаться ответа или опубликовать событие и жить дальше. Выбор делается не один раз на систему, а на каждое взаимодействие — и от него зависит, что произойдёт с операцией, когда сосед недоступен. А он будет недоступен.

Суть размена

Синхронный вызов даёт ответ сейчас ценой temporal coupling: оба сервиса должны быть живы одновременно, латентности складываются, отказ соседа — ваш отказ (смягчаемый resilience-паттернами, но не отменяемый). Событие даёт развязку во времени: издатель не знает и не ждёт получателей, недоступный потребитель догонит позже — ценой eventual consistency и невозможности получить ответ в этом же запросе.

Семь критериев

1. Нужен ли результат для продолжения

Sync, если без ответа операция не может завершиться: проверить остаток перед списанием, посчитать стоимость доставки в корзине, авторизовать платёж.

Async, если результат соседа не нужен здесь: уведомление, обновление поисковой выдачи, начисление бонусов, аналитика. Тест: что покажет пользователю экран, если ответа соседа не будет? Если «ничего, операция не завершилась» — sync; если «то же самое» — async.

2. Допустима ли деградация при отказе соседа

Sync честен, когда без соседа операция всё равно бессмысленна (платёжный шлюз при оплате).

Async обязателен, когда бизнес-операция должна завершаться независимо: заказ оформляется, даже если сервис уведомлений лежит. Если при падении второстепенного сервиса встаёт главный сценарий — связь выбрана неверно.

3. Сколько получателей

Sync — ровно один адресат, и он часть операции.

Async — получателей больше одного или их состав меняется: «заказ оплачен» интересен складу, бонусам, аналитике и тем, кого ещё не придумали. Событие публикуется один раз; новые потребители подключаются без правки издателя.

4. Направление знания

Sync связывает вызывающего с контрактом вызываемого: оформление заказа знает про API склада.

Async инвертирует: владелец заказа публикует факт своего домена («OrderPaid»), не зная, кто слушает. Это та же инверсия зависимостей, поднятая на уровень сервисов — и главный инструмент против сервисов, сцепленных в ком.

5. Пиковые нагрузки

Sync передаёт пик соседу как есть: всплеск заказов — всплеск вызовов склада, и его лимиты становятся вашими.

Async сглаживает: брокер принимает пик, потребитель разгребает в своём темпе. Очередь — естественный буфер (Kafka, RabbitMQ).

6. Порядок и согласованность

Sync даёт согласованность в момент ответа — дальше она тоже расползается, но шаг операции атомарен с точки зрения вызывающего.

Async требует принять eventual consistency: статус «оплачен» доедет до витрины через секунды. Это нормально — если задекларировано в API и UX, и ненормально, если «само как-нибудь» (распределённые паттерны).

7. Эксплуатационная цена

Sync: ничего нового — HTTP, таймауты, circuit breaker.

Async: брокер, outbox, идемпотентные потребители, DLQ, мониторинг lag-а. Если в системе ещё нет брокера, первое асинхронное взаимодействие платит за весь этот фундамент — иногда честнее отложить.

Чек-лист «возьми балл»

Балл async за каждое «да»:

  1. Ответ соседа не нужен для завершения операции.
  2. Операция должна переживать недоступность соседа.
  3. Получателей больше одного (или будут).
  4. Не хочется, чтобы издатель знал контракты потребителей.
  5. Нагрузка пиковая, соседа нужно защищать буфером.
  6. Лаг в секунды приемлем и может быть честно показан.
  7. Брокер и outbox уже есть.

0–2 — синхронный вызов с полным resilience-набором (timeout, retry на идемпотентном, circuit breaker). 3+ — событие через брокер, с outbox на стороне издателя и идемпотентностью на стороне потребителя. Смешанный профиль — это нормально: в одном бизнес-процессе часть шагов синхронна, часть событийна.

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

  • Синхронная сага. Оформление заказа REST-ом дёргает склад, биллинг, доставку и уведомления цепочкой. Доступность перемножается (0.99⁵ ≈ 0.95), латентности складываются, отказ четвёртого отменяет работу трёх. Цепочки шагов — это saga на событиях, не матрёшка вызовов.
  • RPC поверх Kafka. Послать «команду» в топик и ждать ответ в reply-топике с корреляцией — все минусы асинхронности плюс все минусы RPC. Нужен ответ — зовите REST; Kafka — для фактов, не для вопросов.
  • Async там, где пользователь ждёт ответ. «Заказ принят в обработку» вместо результата оплаты, поллинг статуса каждые две секунды. Интерактивные шаги — синхронны по природе.
  • События без outbox. Публикация в брокер после коммита транзакции «обычным вызовом»: упали между — событие потеряно, системы разъехались навсегда.
  • Брокер ради брокера. Kafka между двумя сервисами одной команды с одним потребителем и без пиков — бритва Оккама плачет; REST с retry был бы проще и наблюдаемее.
  • Событие-команда. «SendEmailRequested» в топике — это команда конкретному сервису, замаскированная под событие, со связностью команды и гарантиями события (худшее из двух). События называют свершившиеся факты домена.

Когда оба — нормально

Всегда. Здоровая система — это синхронные запросы там, где пользователь ждёт ответа, и события там, где домены сообщают друг другу о фактах. Подозрительны крайности: система только на REST (ком сцепленных сервисов, падающих вместе) и система только на событиях (включая интерактивные сценарии, где пользователь смотрит на спиннер поллинга).

Что почитать дальше

  • Распределённые паттерны — saga, outbox, idempotency: инструментарий async-стороны.
  • Паттерны отказоустойчивости — timeout, retry, circuit breaker: инструментарий sync-стороны.
  • AMQP vs Kafka — следующая развилка, если выбрали события.
  • Spring Events — та же развилка внутри одного сервиса.