Каждая связь между сервисами — это выбор: позвать 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 за каждое «да»:
- Ответ соседа не нужен для завершения операции.
- Операция должна переживать недоступность соседа.
- Получателей больше одного (или будут).
- Не хочется, чтобы издатель знал контракты потребителей.
- Нагрузка пиковая, соседа нужно защищать буфером.
- Лаг в секунды приемлем и может быть честно показан.
- Брокер и 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 — та же развилка внутри одного сервиса.