Инженерная практика Интеграция: как части системы разговаривают друг с другом
0%

Интеграция: как части системы разговаривают друг с другом

Интеграция: как части системы разговаривают друг с другом

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

Эта глава — про контракт между двумя частями системы. Не только клиент↔сервер, но и сервис↔сервис, сервис↔очередь, сервис↔сторонний вендор. Механика везде одна.

Чего здесь не будет. Разбора HTTP, TCP и TLS по строчкам — это отдельный трек, начиная с «HTTP» и «TLS». Полного руководства по gRPC и GraphQL — за ним в «Стили API» и «RPC и gRPC». Здесь — то, что нужно инженеру, который проектирует стык и отвечает за него в проде.

Интеграция — это про контракт, а не про технологию

Типичный разговор о новом API начинается с «REST или gRPC?». Это неправильный первый вопрос: транспорт и формат — самая дешёвая для замены часть. Дорого стоит контракт, потому что после первого клиента изменить его уже нельзя, а клиентов со временем становится много и не все они ваши.

Контракт — это семь вещей, и только первая из них про технологию.

Составляющая контракта Что нужно зафиксировать Цена ошибки
Формат синтаксис: JSON, protobuf, CSV; кодировки, типы, точность чисел несовместимость на парсинге, потеря точности в деньгах
Семантика что значит каждое поле, в каких единицах, часовой пояс, обязательность тихая порча данных, которую заметят через месяцы
Ошибки какие бывают, как отличить «повтори» от «не повторяй никогда» клиент ретраит невосстановимую ошибку в бесконечном цикле
Гарантии доставки at-most-once, at-least-once, exactly-once (и почему последнее — миф) потерянные или задвоенные операции
Идемпотентность что происходит при повторе того же запроса двойное списание
Эволюция что считается совместимым изменением, как выводят поля из обращения сломанные клиенты при выкладке
Нефункциональные свойства таймауты, лимиты частоты, размер тела, целевая задержка «у вас всё работает, а у нас всё лежит»

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

Полезная привычка: описывать контракт до реализации и вместе с потребителем. Формат описания — OpenAPI, protobuf-схема, AsyncAPI, JSON Schema; главное, чтобы он был машиночитаем и лежал в системе контроля версий рядом с кодом. Подробнее про то, как контракт документируют, — «Документация API» и «Анализ API».

Как менялись способы интеграции

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

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

AJAX. XMLHttpRequest появился в Internet Explorer 5 (1999) как ActiveX-объект для Outlook Web Access, а термин AJAX ввёл Джесси Джеймс Гарретт в статье 2005 года. Запрос отправляет JavaScript, ответ приходит фрагментом — сначала в XML, быстро — в JSON. Именно здесь впервые появляется настоящий контракт данных, отделённый от разметки. Плата — состояние теперь размазано между клиентом и сервером, и его надо синхронизировать.

JSON вместо XML. XML требовал схемы, парсера и многословия; JSON ложился на объектную модель JavaScript напрямую. Выигрыш — простота и объём. Плата — потеря встроенной валидации по схеме (её вернули отдельно через JSON Schema) и вечные грабли: у JSON нет типа даты, а числа — это double, из-за чего идентификаторы длиннее 2^53 приезжают испорченными. Отсюда практическое правило: денежные суммы и большие идентификаторы передавайте строками.

REST как стиль. Рой Филдинг в диссертации 2000 года описал не формат API, а архитектурный стиль: ресурсы с адресами, единообразный интерфейс, отсутствие состояния сессии на сервере, кэшируемость. Выигрыш огромен — предсказуемость и работа всей веб-инфраструктуры (кэши, прокси, балансировщики) без специальных настроек. Плата — то, что индустрия называет REST, обычно им не является: «JSON поверх HTTP с глаголами» это ещё не REST, а над гипермедиа (HATEOAS) в реальных API почти никто не работает.

SPA. Приложение грузится один раз, дальше меняются только данные. Выигрыш — отзывчивость, близкая к десктопной. Плата — вся сложность, ранее жившая на сервере (роутинг, состояние, кэш, права), переехала в браузер, а первая отрисовка стала медленнее.

GraphQL. Facebook (2015) решал конкретную боль мобильных клиентов: экрану нужно семь разных кусочков данных, а REST заставляет делать семь запросов или получать лишнее. Клиент описывает форму ответа сам. Плата — кэширование по URL перестаёт работать, произвольный запрос клиента может оказаться неограниченно дорогим, а N+1 переезжает из ORM в резолверы.

gRPC. Для связи между сервисами удобство браузера не нужно, а нужны схема, скорость и кодогенерация. Protobuf даёт строгий контракт, бинарный формат и стриминг поверх HTTP/2. Плата — из браузера напрямую не работает, отладить curl-ом нельзя, нужен слой инфраструктуры.

Серверный рендеринг и стриминг. Маятник качнулся обратно: разметку снова готовит сервер, но отдаёт её по частям, а клиент «оживляет» интерфейс. Выигрыш — быстрая первая отрисовка и меньше JavaScript. Плата — код теперь исполняется в двух средах, и ошибки гидратации отлаживать больно. Детали — в треке фронтенда, «Роутинг и рендеринг» и «Загрузка данных на клиенте».

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

Синхронно или асинхронно

Самое важное решение на стыке — не формат, а обязан ли вызывающий ждать ответа. Оно определяет устойчивость системы сильнее, чем всё остальное.

Способ Когда уместен Чем платите
Запрос-ответ нужен результат немедленно: проверка баланса, авторизация доступность вашего сервиса не выше, чем у вызываемого
Очередь задач долгая работа: отчёт, конвертация видео, рассылка нужен способ узнать статус; порядок и повторы на вашей совести
Публикация-подписка о факте должны узнать несколько незнакомых вам систем никто не гарантирует, что подписчик успел; отладка сложнее
Стриминг непрерывный поток: котировки, телеметрия, лента событий состояние соединения, backpressure, переподключения

Практическое правило: синхронный вызов — это заимствование чужой доступности. Если ваш сервис синхронно ходит в четыре других, каждый из которых доступен на 99,9%, ваша расчётная доступность — уже около 99,6%, и это без учёта собственных сбоев. Каждый синхронный вызов в цепочке должен быть обоснован: нельзя ли ответить пользователю сразу, а работу доделать асинхронно?

Обратная сторона: асинхронность не бесплатна. Она приносит отложенную согласованность («заказ создан, но в поиске появится через 3 секунды»), необходимость идемпотентности на стороне потребителя, дублирование сообщений и куда более сложную отладку — трассировка через очередь требует явного проброса контекста («Наблюдаемость распределённых систем»). Глубокое погружение — «Событийная архитектура» и «Обмен сообщениями».

Стили API: честное сравнение

Стиль Для чего создан Чем платите Когда НЕ надо
REST ресурсы в вебе, кэшируемость, работа со всей HTTP-инфраструктурой многословность, N запросов на составной экран когда клиенту нужны данные из десяти ресурсов сразу
RPC / gRPC быстрая типизированная связь между сервисами нужна кодогенерация, из браузера — только через прокси публичное API для незнакомых внешних потребителей
GraphQL клиент сам собирает форму ответа, один запрос на экран сложное кэширование, риск дорогих запросов, своя авторизация на поля простое CRUD-API с двумя потребителями
WebSocket / SSE сервер инициирует передачу, длительное соединение состояние соединения, масштабирование, переподключения когда хватает опроса раз в 30 секунд
Webhooks уведомить чужую систему о событии, не спрашивая её постоянно нужны подпись, ретраи, дедупликация на стороне получателя когда получатель за NAT и не имеет публичного адреса

Практический ориентир: REST + OpenAPI по умолчанию для внешних и клиентских API; gRPC для внутренней связи сервисов, где важны схема и задержка; GraphQL — когда потребителей много и все хотят разного, и есть кому сопровождать шлюз. Смешивать нормально: gRPC внутри и REST наружу — распространённая и здоровая конструкция. Развёрнутое сравнение — «Стили API».

Проектирование контракта на практике

Ядро главы. Дальше — конкретные решения, которые принимаются один раз и живут годами.

Ресурсы, глаголы, коды ответов

Именуйте ресурсы существительными во множественном числе, действия выражайте методом: GET /orders, POST /orders, GET /orders/{id}, PATCH /orders/{id}. Действие, не укладывающееся в CRUD, — тоже ресурс: не POST /orders/{id}/doCancel, а POST /orders/{id}/cancellation. Это не эстетика: GET обязан быть безопасным (не менять состояние), PUT и DELETE — идемпотентными; на этих свойствах строится поведение кэшей, прокси и повторов.

Коды ответов — часть контракта, а не украшение:

  • 200 — успех с телом, 201 + Location — создан ресурс, 202 — принято в обработку, 204 — успех без тела;
  • 400 — тело не разобрать, 422 — разобрали, но данные не проходят бизнес-валидацию;
  • 401 — не знаем, кто вы, 403 — знаем и вам нельзя;
  • 404 — нет ресурса, 409 — конфликт состояния (например, заказ уже оплачен);
  • 429 — превышен лимит, обязательно с Retry-After;
  • 5xx — сломались мы; клиент имеет право повторить.

Ключевое различие, которое клиент обязан уметь делать: 4xx повторять бессмысленно, 5xx и 429 — можно и нужно. Именно поэтому антипаттерн HTTP 200 {"error": "..."} так вреден: он лишает клиента, прокси и мониторинг возможности отличить успех от отказа. Ошибка обязана быть ошибкой на уровне протокола.

Формат ошибки

Не изобретайте свой. Есть RFC 9457 «Problem Details for HTTP APIs» (развитие RFC 7807) — медиатип application/problem+json:

{
  "type": "https://api.example.com/problems/insufficient-funds",
  "title": "Недостаточно средств",
  "status": 422,
  "detail": "На счёте 1200.00 RUB, требуется 1500.00 RUB",
  "instance": "/orders/3f1c/payment",
  "trace_id": "0af7651916cd43dd8448eb211c80319c",
  "errors": [
    { "field": "amount", "code": "gt_balance", "message": "Сумма превышает доступный остаток" }
  ]
}

Что здесь важно. type — стабильный идентификатор класса ошибки, по нему клиент ветвит логику (не по тексту title, который завтра переведут на другой язык). detail — для человека. trace_id позволяет связать жалобу пользователя с логами за секунды. Поле errors — расширение для пофайловой валидации формы; RFC явно разрешает добавлять свои поля. И правило безопасности: в detail не должно быть стек-трейсов, SQL-запросов и внутренних адресов — это подарок атакующему.

Пагинация: offset против cursor

Классический ?limit=20&offset=40 понятен и позволяет прыгать на произвольную страницу. У него две беды, и обе проявляются только в проде.

Первая — арифметическая. Данные меняются между запросами. Клиент прочитал страницу 1 (записи 1–20); пока он листал, добавились две новые записи в начало; на странице 2 он снова увидит записи, уже показанные на первой, а какие-то пропустит. Для ленты новостей это выглядит как «дубли и пропажи», для фонового импорта — как потерянные объекты.

Вторая — стоимость. OFFSET 100000 заставляет базу прочитать и отбросить сто тысяч строк: чем дальше страница, тем медленнее запрос.

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

-- Первая страница
SELECT id, created_at, title FROM posts
WHERE feed_id = $1
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Следующая: не «пропусти 20», а «продолжи после этой точки».
-- Составной ключ (created_at, id) нужен, чтобы не сломаться на одинаковых временных метках.
SELECT id, created_at, title FROM posts
WHERE feed_id = $1
  AND (created_at, id) < ($2, $3)   -- значения из курсора предыдущей страницы
ORDER BY created_at DESC, id DESC
LIMIT 20;

Ответ отдаёт следующий курсор, а не номер страницы:

{ "items": [ ], "next_cursor": "eyJ0IjoiMjAyNi0wNy0xNlQxMDowMDowMFoiLCJpZCI6NDIxN30", "has_more": true }

Правило выбора: бесконечная лента и любая выгрузка данных — курсор; административная таблица с номерами страниц, где данные меняются редко, — offset. И всегда фиксируйте limit по умолчанию и максимум: запрос без ограничения рано или поздно прилетит.

Фильтрация, сортировка, разреженные поля

Держите синтаксис узким и предсказуемым: GET /orders?status=paid&created_after=2026-01-01&sort=-created_at. Соблазн сделать «универсальный язык фильтров» заканчивается тем, что вы пишете свою СУБД поверх HTTP — и клиент получает возможность построить запрос, кладущий вашу базу. Разрешайте фильтрацию только по полям, под которые есть индексы, и валидируйте sort по белому списку.

Версионирование

Три подхода, у каждого своя ниша.

В пути (/v1/orders, /v2/orders) — грубо, зато очевидно: видно в логах, в браузере, в конфиге прокси. Годится для крупных редизайнов, происходящих раз в несколько лет. Опасность — «версионирование копипастой»: v2 создают копией всего кода v1, и дальше исправления приходится вносить дважды.

В заголовке (Accept: application/vnd.example.v2+json) — чище с точки зрения REST, но невидимо в обычных инструментах и легко теряется при отладке.

Эволюция без версий — то, что работает лучше всего в 90% случаев. Версия не создаётся вовсе; контракт меняется только совместимо (см. следующий раздел). Мало кто любит это признавать, но большинство «переходов на v2» можно было провести как серию аддитивных изменений — просто это требует дисциплины, а не одного героического рефакторинга.

Спецификация как источник истины

Контракт должен быть машиночитаемым и лежать в репозитории. Тогда из него получается:

  • клиентские SDK на нескольких языках через кодогенерацию — без ручного переписывания моделей;
  • валидация запроса и ответа на границе сервиса (несоответствие спецификации ловится тестом, а не жалобой);
  • документация, которая не расходится с реальностью;
  • моки для команды клиента, которая начинает работу до готовности бэкенда.

Два подхода: design-first (сначала OpenAPI-файл, из него генерируются заготовки и моки) и code-first (спецификация генерируется из аннотаций в коде). Первый лучше, когда потребителей много и надо договариваться заранее; второй — когда сервис внутренний и меняется быстро. Плохо только одно: когда спецификация пишется руками отдельно от кода и тихо устаревает. Как проверять соответствие автоматически — «Тестирование API».

Что делать, когда собеседник не отвечает

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

Четыре механизма, которые работают только вместе.

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

Ретрай. Повторять можно только идемпотентные операции. Между попытками — экспоненциально растущая пауза, обязательно со случайным разбросом (джиттером), иначе тысяча клиентов, отвалившихся одновременно, одновременно же вернётся и добьёт сервис — это называется thundering herd. Число попыток небольшое (2–3); ретраи на каждом уровне цепочки перемножаются, и три уровня по три попытки — это уже 27 запросов.

Идемпотентный ключ. Клиент генерирует уникальный ключ на бизнес-операцию и передаёт его в заголовке; сервер запоминает результат по ключу и на повтор возвращает сохранённый ответ, не выполняя работу заново. Так делает Stripe и практически все платёжные системы. Ключ генерирует клиент — иначе он не переживёт повторную отправку.

Circuit breaker. Если сервис явно лежит, продолжать долбить его — вредно для обеих сторон: вы копите ждущие потоки, он не может подняться. Размыкатель считает долю ошибок и на время перестаёт пропускать вызовы вообще.

Вот как это выглядит в коде. Обратите внимание: ключ идемпотентности создаётся один раз на операцию и переиспользуется всеми попытками — в этом весь смысл.

import random
import time
import uuid

import httpx

RETRIABLE = {408, 425, 429, 500, 502, 503, 504}

def create_payment(order_id: int, amount: str, attempts: int = 3) -> dict:
    """Повторяет запрос с экспоненциальной паузой и джиттером.

    Ключ идемпотентности один на всю операцию: сервер отличит повтор
    от нового платежа и не спишет деньги дважды.
    """
    idempotency_key = str(uuid.uuid4())
    base_delay = 0.2          # стартовая пауза, секунды
    last_error: Exception | None = None

    for attempt in range(attempts):
        try:
            response = httpx.post(
                "https://payments.internal/v1/payments",
                json={"order_id": order_id, "amount": amount},
                headers={"Idempotency-Key": idempotency_key},
                # connect и read разводим: недоступность и медлительность — разные беды
                timeout=httpx.Timeout(connect=1.0, read=2.0, write=2.0, pool=1.0),
            )
        except httpx.TimeoutException as exc:
            last_error = exc      # таймаут — повторяем: запрос мог и не дойти
        else:
            if response.status_code < 400:
                return response.json()
            if response.status_code not in RETRIABLE:
                # 4xx: повтор ничего не изменит, ошибка в нашем запросе
                response.raise_for_status()
            last_error = httpx.HTTPStatusError(
                f"HTTP {response.status_code}", request=response.request, response=response
            )
            # сервер сам сказал, сколько ждать, — уважаем
            retry_after = response.headers.get("Retry-After")
            if retry_after and retry_after.isdigit():
                time.sleep(int(retry_after))
                continue

        if attempt < attempts - 1:
            # full jitter: пауза равномерно случайна в [0, base * 2^attempt].
            # Без джиттера все отвалившиеся клиенты вернутся одной волной.
            time.sleep(random.uniform(0, base_delay * (2 ** attempt)))

    raise RuntimeError(f"Платёж не создан за {attempts} попыток") from last_error

Про джиттер стоит прочитать первоисточник — заметку AWS «Exponential Backoff And Jitter»: там показано на моделировании, почему «full jitter» выигрывает у ретраев с фиксированной паузой. Полный разбор паттернов — «Шаблоны устойчивости» и «Идемпотентность и гарантии доставки».

Асинхронный стык выглядит иначе, и у него свой набор гарантий:

Здесь ключевых моментов три: событие пишется в той же транзакции, что и данные (иначе рано или поздно получите заказ без события или событие без заказа); доставка at-least-once, то есть дубли неизбежны и потребитель обязан быть идемпотентным; неудачные сообщения уходят в отдельную очередь, а не крутятся вечно, отравляя обработку.

Эволюция контракта без поломки клиентов

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

Совместимые (аддитивные) изменения — добавить необязательное поле в ответ, добавить необязательный параметр запроса с прежним поведением по умолчанию, добавить новое значение в перечисление, если клиенты готовы к неизвестным значениям, добавить новый эндпоинт.

Несовместимые — удалить или переименовать поле, сделать необязательное поле обязательным, поменять тип или единицу измерения, изменить смысл поля при том же имени (самое коварное: ломается тихо), изменить код ответа для существующего сценария, ужесточить валидацию.

Tolerant reader — правило для потребителя, сформулированное ещё Мартином Фаулером (Tolerant Reader): читай только те поля, которые тебе нужны, игнорируй незнакомые, не падай на неизвестном значении перечисления, не полагайся на порядок элементов. Клиент, написанный так, переживёт большинство изменений на той стороне без единой правки. Обратная сторона — правило Постела не абсолютно: чрезмерная снисходительность к мусору на входе прячет ошибки. Читайте терпимо, но валидируйте то, на что действительно опираетесь.

Вывод из обращения по расписанию, а не по факту. У HTTP для этого есть стандартные заголовки: RFC 9745 вводит Deprecation, а RFC 8594Sunset:

HTTP/1.1 200 OK
Deprecation: @1767225600
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Link: <https://api.example.com/docs/migrations/orders-v2>; rel="deprecation"

Дальше — обычная работа: метрики по устаревшему эндпоинту с разбивкой по клиентам (иначе вы не знаете, кого отключаете), уведомление потребителей, отключение только после того, как трафик сошёл на нет.

Контрактные тесты переводят договорённость из вики в CI. Потребитель описывает, что именно он ожидает от провайдера; провайдер прогоняет эти ожидания на своей сборке и падает, если сломал их. Так ломающее изменение обнаруживается в пайплайне, а не в проде. Инструмент-эталон — Pact; подробнее — «Тестирование API» и «Совместимость и эволюция контрактов».

Безопасность на стыке

Граница системы — единственное место, где вход не под вашим контролем, поэтому здесь минимум четыре обязательные вещи.

Аутентификация сервис-сервис. «Мы внутри периметра, у нас всё доверенное» — модель, которую перестали считать приемлемой: один скомпрометированный под получает доступ ко всему. Рабочие варианты — mTLS (взаимные сертификаты, обычно выдаёт service mesh) и OAuth 2.0 client credentials с короткоживущими токенами. Пользовательский JWT не должен пробрасываться дальше первого сервиса как есть — это расширение области доверия («JWT и токены», «OAuth и OIDC»).

CORS. Постоянный источник недопонимания. Механизм ничего не защищает на сервере — он лишь позволяет браузеру разрешить чужому origin читать ваш ответ. Ошибка CORS в консоли означает «браузер не дал прочитать», а не «запрос не дошёл»: небезопасный запрос вполне мог выполниться. Отсюда два вывода: Access-Control-Allow-Origin: * вместе с куками не работает и работать не должен, а CORS не заменяет авторизацию и защиту от CSRF («XSS и CSRF»).

Ограничение скорости. Лимит на клиента и на эндпоинт с честным 429 и Retry-After — это защита не только от злоумышленника, но и от собственного клиента с багом в цикле ретраев. Заодно — ограничение размера тела запроса, глубины вложенности JSON и сложности GraphQL-запроса.

Не отдавайте наружу внутреннюю модель. Автоматическая сериализация ORM-сущности в JSON — быстрый способ однажды опубликовать хеш пароля, внутренний комментарий или служебный флаг. Явный слой представления (DTO) с белым списком полей стоит десяти строк и снимает целый класс утечек. Он же развязывает схему БД и контракт API: колонку можно переименовать, не ломая клиентов.

Систематически — «Безопасность API».

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

  • HTTP 200 с полем "error" внутри. Ломает всё, что смотрит на статус: мониторинг, ретраи, кэш, балансировщик. Ошибка обязана быть ошибкой протокола.
  • Ретрай неидемпотентного POST. Классика двойных списаний. Либо идемпотентный ключ, либо никаких повторов.
  • Отсутствие таймаута. Один зависший вызов выедает пул потоков и превращает частичную деградацию в полный отказ.
  • Версионирование копипастой. v2 как копия кода v1: два места для каждой правки и гарантированное расхождение поведения.
  • «Отдадим наружу всю модель БД». Контракт становится заложником схемы: любой рефакторинг таблицы ломает клиентов.
  • Ретраи на каждом уровне. Клиент, шлюз и сервис повторяют независимо — нагрузка на упавший сервис растёт кратно. Повторяет кто-то один, обычно самый внешний.
  • Пагинация через offset на изменяющихся данных — дубли, пропуски и медленные страницы в глубине выборки.
  • Ошибки без машиночитаемого кода. Клиент вынужден разбирать текст сообщения — и ломается при первой же правке формулировки.
  • Событие публикуется до COMMIT. Подписчик обращается за данными, которых ещё (или уже) нет.
  • Незадокументированный контракт с фразой «это же внутренний сервис». Внутренние потребители ломаются так же, как внешние.

Мини-итог

  • Интеграция — это контракт: формат, семантика, ошибки, гарантии доставки, идемпотентность, эволюция и нефункциональные свойства. Технология выбирается последней.
  • История стыка клиент↔сервер — маятник: сложность переезжает с сервера на клиент и обратно. Спрашивайте не «что новее», а «куда переедет сложность».
  • Синхронный вызов заимствует чужую доступность. Всё, что можно сделать асинхронно, лучше сделать асинхронно — заплатив отложенной согласованностью.
  • REST + OpenAPI по умолчанию наружу, gRPC внутрь, GraphQL — когда потребителей много и все хотят разного.
  • Ошибки — по RFC 9457, пагинация на изменяющихся данных — курсорная, версионирование — по возможности эволюционное.
  • Таймаут, ограниченный ретрай с джиттером, идемпотентный ключ и circuit breaker работают только вместе.
  • Совместимые изменения аддитивны; потребитель — tolerant reader; вывод из обращения идёт по Deprecation/Sunset и подтверждается метриками, а договорённость проверяется контрактными тестами в CI.

Источники

Что дальше

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

Дальше — Архитектурные шаблоны на практике.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов