коротко

Синхронная интеграция — это «спросил и жду ответ прямо сейчас» (обычный REST-запрос): просто, но если сервис на той стороне тормозит или упал — ждёт и падает и ваша операция. Асинхронная — «отправил сообщение и пошёл дальше», ответ придёт потом, через очередь, событие или вебхук: устойчивее и масштабируемее, но сложнее. В асинхронном мире сообщение может прийти дважды, поэтому критичные операции (платёж, заказ) должны быть идемпотентными — повторный одинаковый запрос не создаёт второй платёж. Очереди (например, Kafka) развязывают сервисы во времени, но дают доставку «хотя бы один раз» — отсюда и нужна идемпотентность.

Самый дорогой баг в моей практике стоил клиенту двух списаний за один заказ. Платёжный сервис отвечал медленно, наш бэкенд не дожидался ответа и повторял запрос — а первый запрос на самом деле проходил. Деньги списывались дважды. Чинили не код отправки, а саму модель: научили платёж быть идемпотентным. Тогда я понял, что «синхронно или асинхронно» — это не про технологию, а про то, что произойдёт, когда что-то пойдёт не так. А пойдёт обязательно.

Синхронно и асинхронно: в чём разница

Разница — в ожидании. Если механизма REST вы ещё не видели, загляните в запись про REST API; здесь он будет примером синхронного вызова.

Синхронный вызов — клиент отправляет запрос и блокируется: стоит и ждёт ответ, прежде чем продолжить. Как телефонный звонок — вы не вешаете трубку, пока собеседник не ответит. Классический пример: REST-запрос «создай заказ — жду подтверждения».

Асинхронный вызов — клиент отправляет сообщение и сразу идёт дальше, не дожидаясь результата. Как письмо или СМС — кинул и занялся своими делами, ответ придёт когда-нибудь потом. Сообщение обычно кладётся в очередь, а получатель забирает его в своём темпе.

sequenceDiagram
  participant A as Сервис A
  participant B as Сервис B
  participant Q as Очередь
  Note over A,B: Синхронно
  A->>B: запрос
  B-->>A: ответ (A всё это время ждёт)
  Note over A,Q: Асинхронно
  A->>Q: положил сообщение
  Q-->>A: принято (A сразу свободен)
  Q->>B: B заберёт, когда сможет

Схема показывает оба сценария. В синхронном случае сервис A отправляет запрос напрямую сервису B и блокируется до ответа — всё время, пока B думает, A простаивает и зависит от него. В асинхронном A кладёт сообщение в очередь и немедленно получает «принято», после чего свободен заниматься другим; сервис B заберёт сообщение из очереди тогда, когда будет готов, — может через миллисекунду, может через минуту. Главное отличие: в синхронном мире A и B жёстко связаны во времени, в асинхронном — развязаны через посредника.

Когда выбирать что

Это классический trade-off: простота против устойчивости и масштаба.

Синхронно — когда ответ нужен здесь и сейчас, чтобы продолжить. Пользователь вводит логин и пароль — без ответа «верно/неверно» дальше идти некуда. Когда операций немного и сервисы надёжны, синхронный REST проще: легче читать, отлаживать, понимать. Цена — связанность: если сервис на той стороне медленный или лежит, лежит и ваша операция.

Асинхронно — когда ответ не нужен немедленно, когда нагрузка скачет, когда одно событие интересно многим. Пользователь оформил заказ — ему не обязательно ждать, пока сработают начисление бонусов, отправка письма и обновление склада. Положили событие «заказ создан» в очередь, ответили пользователю «принято», а остальное досчиталось в фоне. Цена — сложность: появляются очереди, повторы, порядок сообщений, отслеживание «а оно вообще доехало».

Простое правило выбора

Нужен ответ, чтобы продолжить прямо сейчас, → синхронно. Можно ответить «приняли, сделаем» и досчитать в фоне → асинхронно. Если сомневаетесь — начинайте с синхронного: его проще понять и отладить, а на асинхронный перейдёте, когда упрётесь в нагрузку или связанность. Классический асинхронный случай — медленные вызовы к языковой модели: их не ждут синхронно в интерфейсе, а показывают прогресс.

Идемпотентность простыми словами

Идемпотентность — это свойство операции давать один и тот же результат, сколько бы раз её ни повторили. Нажать кнопку лифта пять раз — лифт приедет один раз: кнопка идемпотентна. А «снять 100 рублей со счёта» при пяти повторах снимет 500 — это не идемпотентно, и именно тут рождаются двойные списания.

Почему это вообще проблема: в сети запросы теряются и повторяются. Клиент отправил «создай заказ», не дождался ответа (сеть моргнула) и повторил. А первый запрос на самом деле дошёл. Без защиты получаются два заказа. С защитой — один.

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

POST /payments
Idempotency-Key: 7f3e-a91c-2024-0042

{ "orderId": 123, "amount": 1500 }

Сервер запоминает этот ключ. Пришёл запрос с новым ключом — выполняет и сохраняет результат. Пришёл повтор с тем же ключом — не выполняет заново, а возвращает уже посчитанный результат. Так повтор становится безопасным: операция в системе случается ровно один раз, сколько бы раз клиент ни нажал.

Это вопрос аналитика, а не «магия бэка»

Идемпотентность не появляется сама. Кто-то должен решить: какие операции критичны (платёж, заказ, списание), кто генерирует ключ, как долго сервер его хранит, что вернуть на повтор. Это требования, и писать их — ваша работа. Если в спеке на платёж нет слова «идемпотентность» — спека дырявая.

Вебхуки: «не звони мне, я сам позвоню»

Бывает обратная задача: вам нужен ответ от внешнего сервиса, но не сразу, а когда у него что-то произойдёт. Платёж может обрабатываться минутами. Опрашивать сервис каждую секунду «ну как, готово?» — расточительно. Тут помогает вебхук.

Вебхук — это перевёрнутый запрос. Вместо того чтобы вы дёргали чужой API, вы регистрируете у него свой URL, и когда событие наступает, он сам присылает вам HTTP-запрос. «Платёж прошёл» — банк стучится на ваш /webhooks/payment и сообщает об этом. Это асинхронность через обычный HTTP: получатель узнаёт о событии в момент, когда оно случилось, а не когда в следующий раз догадается спросить.

Очереди и Kafka на пальцах

Когда сообщений много и важно ничего не потерять, между сервисами ставят очередь — посредника, который принимает сообщения от одних и отдаёт другим. Разберём на словаре Kafka, самой известной системы такого рода.

  • Продюсер (producer) — тот, кто пишет сообщения. Наш сервис заказов кладёт событие «заказ создан».
  • Топик (topic) — именованный поток сообщений, как канал. Например, топик orders. Сообщения в нём хранятся какое-то время, а не исчезают сразу после прочтения.
  • Консьюмер (consumer) — тот, кто читает. Сервис бонусов и сервис уведомлений независимо читают топик orders и каждый делает своё.
flowchart LR
  P["Продюсер: сервис заказов"] -->|"заказ создан"| T["Топик: orders"]
  T --> C1["Консьюмер: бонусы"]
  T --> C2["Консьюмер: уведомления"]
  T --> C3["Консьюмер: склад"]

На схеме сервис заказов (продюсер) пишет одно событие «заказ создан» в топик orders. Из этого топика событие независимо забирают три консьюмера — сервисы бонусов, уведомлений и склада, — и каждый делает свою часть работы, не мешая остальным и не зная друг о друге. В этом сила очереди: один отправитель, много получателей, все развязаны. Захотим завтра добавить четвёртый сервис — подпишем его на тот же топик, и отправителя менять не придётся. Когда всё общение системы строят вокруг таких фактов-событий, это называют событийной архитектурой.

At-least-once и почему без идемпотентности нельзя

Очереди вроде Kafka обычно гарантируют доставку «хотя бы один раз» (at-least-once): сообщение точно не потеряется, но в редких случаях может прийти дважды — например, консьюмер обработал его, но не успел подтвердить, и очередь прислала повторно. Поэтому асинхронность и идемпотентность ходят парой: раз сообщение может задвоиться, обработчик обязан быть готов к повтору. Это не теория — это про те самые двойные списания.

Откуда это взялось

Термин webhook придумал Джефф Линдсей в 2007 году — как идею «обратных вызовов для веба», когда сервис сам уведомляет подписчиков о событии. Kafka родилась внутри LinkedIn около 2011 года: компании нужно было прокачивать гигантские потоки событий между десятками сервисов, и обычные очереди не тянули. Решение отдали в открытый код, и сегодня Kafka — фактический стандарт для потоков событий. Обе технологии решают одну боль — развязать сервисы во времени, — но с разных сторон: вебхук для «один отправитель, один-два получателя по HTTP», очередь — для «много сообщений, много получателей, ничего не терять».

Частые вопросы

Что такое идемпотентность простыми словами?

Это свойство операции давать один и тот же результат при любом числе повторов. Нажать кнопку лифта пять раз — лифт приедет один раз. В интеграциях это критично для платежей и заказов: если запрос повторился из-за сбоя сети, идемпотентная операция не создаст второй платёж. Достигается это идемпотентным ключом — уникальным id операции, по которому сервер распознаёт повтор и возвращает прежний результат вместо повторного выполнения.

Когда использовать очередь, а когда REST?

REST (синхронно) — когда ответ нужен немедленно, чтобы продолжить: логин, проверка, чтение данных. Очередь (асинхронно) — когда ответ не нужен сразу, нагрузка скачет или одно событие интересно нескольким сервисам: оформление заказа с фоновыми начислениями, рассылками, обновлением склада. Правило: нужен ответ прямо сейчас → REST; можно ответить «приняли» и досчитать в фоне → очередь.

Что такое вебхук (webhook)?

Вебхук — это перевёрнутый запрос: вместо того чтобы вы опрашивали чужой сервис «готово ли?», вы регистрируете у него свой URL, и сервис сам присылает вам HTTP-запрос, когда наступает событие. Например, платёжный провайдер стучится на ваш адрес, когда платёж прошёл. Это способ узнавать о событиях в момент их наступления, не дёргая чужой API в цикле.