Синхронная интеграция — это «спросил и жду ответ прямо сейчас» (обычный 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 в цикле.