Тестирование и анти-паттерны¶
Тестирование¶
Распределёнка ломается не там, где зелёный юнит-тест. Юниты проходят, сервис собирается, а потом два сервиса не сходятся в формате одного поля — и узнаёшь об этом в проде. Главная боль микросервисов — не логика внутри сервиса, а интеграция между сервисами. Вокруг этого и строишь тестовую стратегию.
Пирамида тестов
Идея простая: чем выше тест, тем он медленнее, дороже и хрупче. Значит, основную массу проверок держишь внизу, а наверху — минимум.
/\
/ \ e2e мало, дорого, флака
/----\
/ \ slice / средний слой,
/ \ integration реальные зависимости
/----------\
/ \ unit много, быстро, дёшево
/--------------\
- unit — чистая логика без сети и БД. Миллисекунды, тысячи штук, гоняются на каждый коммит. Здесь проверяешь бизнес-правила, а не интеграцию.
- slice / integration — кусок сервиса с реальными зависимостями: своя БД, брокер, кэш. Тут ловишь
то, что моки скрывают: реальный SQL, реальную сериализацию в
Kafka. - e2e — весь сценарий через несколько поднятых сервисов. Даёт уверенность в happy-path, но медленный, хрупкий и дорогой в поддержке. Держишь штук пять на критичные потоки, не больше.
Грабли: перевернуть пирамиду в «песочные часы» или «рожок мороженого» — когда e2e много, а unit мало. Тогда CI идёт полчаса, падает по флаке, и никто не верит красному прогону.
Testcontainers для slice-тестов
Моки БД и брокера врут. In-memory H2 вместо Postgres не воспроизводит специфику диалекта, а
замоканный producer не покажет, что схема сообщения поехала.
Testcontainers — это библиотека, которая поднимает реальные зависимости в Docker на время теста и гасит их после.
Что делает:
- стартует контейнер с реальным Postgres, Kafka, Redis на случайном порту;
- отдаёт тесту connection string уже поднятого инстанса;
- сносит контейнер в teardown — никакого мусора между прогонами.
func TestOrderRepo(t *testing.T) {
ctx := context.Background()
pg, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("orders"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
require.NoError(t, err)
defer pg.Terminate(ctx)
dsn, _ := pg.ConnectionString(ctx, "sslmode=disable")
repo := NewOrderRepo(mustOpen(dsn))
// дальше — реальный SQL против реального Postgres
}
Плюсы: тест видит ровно ту БД, что в проде, вместе с диалектом, индексами и миграциями. Минусы: нужен Docker в CI, прогон в секундах, не в миллисекундах. Поэтому это средний слой пирамиды, а не замена юнитам.
Contract testing: интеграция без поднятия всей системы
Главный вопрос: провайдер выкатил новую версию API — кто из потребителей сломался? e2e ответит, но дорого и поздно. Contract testing отвечает дёшево и в CI каждого сервиса по отдельности.
Consumer-Driven Contracts — это паттерн эволюции сервисов, где ожидания каждого потребителя фиксируются контрактом, а провайдер обязан их поддерживать, чтобы не сломать потребителей. Описал Ian Robinson в 2006 (статья на сайте Фаулера).
Идея простая: контракт пишет не провайдер, а потребитель. Потребитель говорит «я шлю вот такой запрос и жду вот такой ответ». Провайдер у себя в CI проверяет, что всё ещё отдаёт обещанное.
Как это работает по шагам:
1. Потребитель в своих тестах гоняет запросы против мока провайдера и фиксирует ожидания как
контракт (Pact пишет его в JSON).
2. Контракт публикуется в общее место (Pact Broker).
3. Провайдер в своём CI проигрывает контракт против реального себя — verification.
4. Провайдер сломал поле — его CI краснеет, до прода не доходит.
Consumer CI Pact Broker Provider CI
----------- ----------- -----------
прогнал тесты --pact--> хранит <--pull-- провёл verification
зафиксировал контракты против реального сервиса
ожидания + результаты красный, если сломал контракт
Чем отличается от схемы провайдера: не провайдер диктует «вот мой OpenAPI», а потребители диктуют «вот что нам реально нужно». Поля, которые ни один потребитель не использует, можно менять свободно — контракт их не покрывает.
Инструменты: Pact (язык-агностик, через broker) и Spring Cloud Contract (JVM-мир).
Грабли:
- путать Pact с самим паттерном — Pact лишь одна реализация Consumer-Driven Contracts;
- считать, что contract testing полностью заменяет e2e — он проверяет совместимость пары
consumer-provider, но не сквозной бизнес-сценарий через пять сервисов;
- для async-коммуникации контракт описывает не запрос-ответ, а формат сообщения в топике — это
тоже зона contract testing, не только REST. Детали протоколов — в Communication & API Style.
Note
Schema-evolution через Schema Registry (Avro/Protobuf, backward/forward compatibility) — это
контроль совместимости на уровне wire-формата. Близкая идея, но другой слой: registry проверяет
схему сообщения, contract testing — взаимные ожидания пары сервисов.
Migration tests в CI
Database per Service означает, что у каждого сервиса своя схема и свои миграции. Сломанная
миграция кладёт деплой. Поэтому миграции прогоняют в CI, а не на проде в момент выката.
Что делать:
- поднимаешь чистую БД в Docker (те же Testcontainers);
- прогоняешь все миграции Flyway или Liquibase от нуля;
- проверяешь, что схема собралась и что новые миграции накатываются поверх старых.
Зачем гонять от нуля: ловишь миграцию, которая работает только на твоей локальной БД с ручными правками, но падает на чистой. Грубо говоря, CI воспроизводит то, что случится на свежем инстансе.
Грабли: тестировать только «вперёд». Если есть rollback-сценарий — проверяй и откат, иначе в инцидент откатиться будет нечем.
Chaos Engineering
Юнит и contract проверяют логику и совместимость. Но в проде сервис падает по другим причинам: инстанс умер, сеть подвисла, зависимость отвечает за 30 секунд вместо 30 миллисекунд. Это тестируют отдельно.
Chaos Engineering — это дисциплина, где намеренно вносишь реалистичные сбои, чтобы набрать
уверенность, что система переживёт турбулентность в проде. Netflix запустил Chaos Monkey в 2011,
принципы оформили в 2015.
Идея простая: не ждать, пока инстанс умрёт сам в 3 ночи, а убить его днём, когда команда смотрит. Переживает — хорошо. Нет — чинишь, пока не больно.
Что ломают:
- убивают инстансы (Chaos Monkey в оригинале — именно это);
- режут или замедляют сеть между сервисами;
- роняют зависимость и смотрят, сработали ли circuit breaker, retry, timeout.
Важный нюанс: это не «рандомно ломать прод». Канонически — контролируемый эксперимент с гипотезой.
Определяешь steady state (как система выглядит здоровой), формулируешь гипотезу («при смерти одного
инстанса latency не вырастет»), ограничиваешь blast radius и проверяешь. Chaos Monkey — один
инструмент, а не вся дисциплина.
Связь с устойчивостью: chaos проверяет ровно те паттерны, что описаны в Macro-Architecture — circuit breaker, bulkhead, retry. Без них chaos просто роняет систему. С ними — подтверждает, что защита работает.
Грабли тестирования распределёнки
| Симптом | Что не так | Что делать |
|---|---|---|
| CI идёт полчаса, флака | перевёрнутая пирамида, всё через e2e | основную массу — в unit и slice, e2e только на критичные потоки |
| «у нас интеграция проверяется в проде» | нет contract testing | consumer-driven contracts в CI каждого сервиса |
| деплой падает на миграции | миграции не гоняются в CI | Flyway/Liquibase от нуля на чистой БД в каждом прогоне |
| моки БД зелёные, прод красный | in-memory вместо реальной БД | Testcontainers с реальным образом |
| инцидент от смерти инстанса | устойчивость не проверена | chaos-эксперимент с гипотезой и blast radius |
Если коротко: основную массу проверок держи внизу пирамиды, интеграцию лови contract-тестами до прода, а устойчивость — chaos-экспериментами, а не в момент реального инцидента.
Анти-паттерны¶
Микросервисы — это не про много мелких сервисов. Это про автономию: independent deployability, своя БД, своя команда. Большинство анти-паттернов — места, где автономию незаметно потеряли. Снаружи топология выглядит как микросервисы, внутри связность как у монолита, но без его плюсов.
Дальше — частые грабли. Для каждого симптом и чем лечить.
Distributed Monolith
Сервисы нарезаны, но деплоятся и ломаются как единое целое. Худший из миров: сетевые задержки распределённой системы плюс жёсткая связность монолита. Детальный разбор и стратегия выхода — в Macro-Architecture.
Симптом: - релиз одного сервиса требует синхронного релиза трёх соседей, иначе ломается контракт. - общая БД, в которую несколько сервисов лезут напрямую. - любой пользовательский запрос идёт по sync-цепочке через половину сервисов. - падение одного сервиса роняет фичу, которая логически от него не зависит.
Чем лечить:
- режь по business capability, не по таблицам (см. Macro-Architecture).
- каждому сервису — своя приватная БД (Database per Service), доступ к чужим данным только через API.
- разрывай sync-цепочки async-событиями там, где консистентность может быть eventual.
Shared database
Несколько сервисов пишут и читают одну схему. Это корень почти всех остальных проблем — из общей схемы distributed monolith вырастает сам собой.
Чем плохо: - схему нельзя менять, не сломав соседей. Миграция превращается в кросс-командную операцию. - нет инкапсуляции: чужой сервис знает твою внутреннюю структуру таблиц и завязан на неё. - транзакционные границы размазаны — непонятно, кто владеет инвариантом. - нельзя независимо масштабировать или сменить движок под нагрузку конкретного сервиса.
Чем лечить:
- Database per Service — приватная схема на сервис, чужой код в неё не ходит.
- нужны чужие данные синхронно — спрашивай через API. Нужны асинхронно — подпишись на события.
- разносить общую БД больно, но раньше начнёшь — дешевле. Часто помогает выделять по одной таблице
за раз через Strangler Fig.
Note
Один физический инстанс БД на несколько сервисов — это ещё не shared database, если у каждого
своя схема и никто не лезет в чужую. Database per Service — про логическую изоляцию владения,
а не про обязательный отдельный сервер. Анти-паттерн — общая схема и общие таблицы.
Chatty communication и нано-сервисы
Сервисы нарезаны слишком мелко. Один бизнес-запрос разворачивается в десятки сетевых хопов, каждый сервис делает почти ничего.
Симптом: - чтобы собрать одну страницу, gateway дёргает 15 сервисов. - сервисы постоянно ходят друг к другу за мелкими кусочками данных в горячем пути. - latency складывается из сетевых round-trip'ов, а не из реальной работы.
плохо: один запрос = N синхронных хопов
client -> A -> B -> C -> D -> E (latency = сумма всех, отказ любого = отказ всего)
хорошо: укрупнить границы + меньше хопов в горячем пути
client -> A ----> B
\-> C (A держит то, что нужно часто; остальное async)
Чем лечить: - укрупни границы: сервис владеет capability целиком, а не одним методом. - то, что нужно в горячем пути часто, держи локально (своя проекция данных, read model). - мелкие чтения через сеть заменяй на data ownership или кэш на стороне потребителя.
Note
Граница сервиса — про связность данных и команду-владельца, а не про размер кода. «Микро» — не про строки. Сервис на 50 тысяч строк с одним владельцем нормальнее, чем десять «нано» вокруг одной сущности.
Длинная sync-цепочка
Запрос идёт A -> B -> C -> D синхронно, каждый блокирующе ждёт следующего. Latency и вероятности
отказа перемножаются — цепочка надёжна ровно настолько, насколько надёжно произведение звеньев.
Симптом: - p99 хвост одного звена раздувает p99 всей цепочки. - timeout или деградация в конце цепочки каскадом бьёт по всем выше. - доступность цепочки = произведение доступностей. Четыре звена по 99.9% дают уже ~99.6%.
Чем лечить:
- где консистентность может быть eventual — заменяй синхронный вызов на async-событие или Saga.
- обязательные синхронные вызовы оборачивай в Circuit Breaker, Timeout и bulkhead, чтобы отказ
не каскадировал.
- укрупняй границы, чтобы цепочка была короче. Детали протоколов sync-вызова — в Communication & API Style.
Entity services
Сервис построен вокруг таблицы или сущности (UserService, OrderRowService), а не вокруг
бизнес-capability. CRUD-обёртка над строкой, у которой нет своего поведения.
Чем плохо: - любая реальная операция размазана по нескольким entity-сервисам и требует их оркестрации. - бизнес-логика утекает наружу — в gateway или в вызывающий код, потому что в сервисе её нет. - получается тот же chatty-антипаттерн: чтобы сделать заказ, дёргаешь пять сущностных сервисов.
Чем лечить: - режь по business capability: «оформление заказа», а не «строка в таблице orders». - сервис должен владеть и данными, и поведением над ними — инвариант живёт внутри. - если сервис умеет только CRUD и не содержит правил — это, скорее всего, не граница, а таблица.
God gateway
Gateway оброс бизнес-логикой: оркестрация бизнес-процессов, валидация доменных правил, трансформации, которые знают про домен. Получился монолит на входе, через который проходит и от которого зависит всё.
Симптом: - любая новая фича требует правки gateway, и его релизит отдельная команда-бутылочное горлышко. - gateway знает доменные правила сервисов и ломается, когда меняется их логика. - падение gateway = падение всего, и откатывать его страшно.
Чем лечить:
- gateway держит только cross-cutting: routing, auth, rate limiting, агрегацию ответов.
- бизнес-логику и оркестрацию верни в сервисы, которым она принадлежит.
- много разных клиентов с разными нуждами — заводи Backends for Frontends, а не раздувай один gateway.
- роль и границы gateway/BFF подробно — в Communication & API Style.
Shared доменная библиотека
Сервисы тащат общую библиотеку с доменными моделями и бизнес-логикой. Правишь её — и нужно
передеплоить всех, кто её подключает. Independent deployability убита одним import.
Чем плохо: - общий доменный код связывает сервисы по compile-time. Это shared database, только в другом слое. - версионный ад: либо все на одной версии (передеплой пачкой), либо зоопарк несовместимых версий. - доменная модель одного сервиса протекает в другой — границы bounded context размываются.
Чем лечить:
- доменную модель не шарь — у каждого сервиса своя, даже если поначалу похожи. Дублирование здесь
дешевле связности.
- общими делай только технические, доменно-нейтральные вещи: логирование, трейсинг, сериализация.
- интеграцию строй через контракты на границе (события, API), а не через общий код. Чужую модель
на входе изолируй Anti-Corruption Layer.
Note
Тест простой: можешь ли выкатить новую версию библиотеки, не трогая потребителей? Для технической утилиты — да. Для доменной модели — почти никогда, и поэтому её нельзя шарить.
Если коротко:
| Симптом | Анти-паттерн | Чем лечить |
|---|---|---|
| Релиз пачкой, общая БД, sync-цепочки | Distributed Monolith | автономия: своя БД, async-границы (Macro-Architecture) |
| Несколько сервисов в одной схеме | Shared database | Database per Service, доступ через API |
| Один запрос = десятки хопов | Chatty / нано-сервисы | укрупнить границы, локальная read model |
A->B->C->D блокирующе |
Длинная sync-цепочка | async/Saga, Circuit Breaker, Timeout |
| Сервис на таблицу | Entity services | резать по business capability |
| Бизнес-логика в gateway | God gateway | только cross-cutting, логику в сервисы |
| Общий код с доменом | Shared доменная либа | шарить только технику, домен дублировать |
Чек-лист и если коротко¶
Прежде чем дробить — проверяешь готовность. Не по хайпу, а по факту. Если на большинство пунктов честный ответ «нет» — рано. Сиди на modular monolith (Macro-Architecture), он дешевле в разы.
Прежде чем дробить
- Знаешь bounded contexts? — Не папки в проекте, а границы модели, внутри которых термин значит
одно (
Bounded Context, Eric Evans, 2003). Если границы размыты — нарежешь сервисы по случайным линиям и получишь distributed monolith (Macro-Architecture). Сначала subdomains и context map, потом код. - БД развяжешь? — Database per Service означает приватное владение данными: чужой сервис лезет только через API, не в твою схему. Это логическая изоляция, не обязательно отдельный инстанс — допустимы schema-per-service или table-per-service. Развязка данных обычно больнее развязки кода.
- Есть CI/CD на каждый сервис? — Независимый деплой — это и есть смысл микросервисов, а не размер. Если катишь всё одним релизом — дробление не дало ничего, только добавило сетевых вызовов.
- Есть observability? — Distributed tracing, метрики, централизованные логи (
OpenTelemetry). Без них упавший запрос через пять сервисов — слепой дебаг по логам наугад. - Есть on-call и runbooks? — Микросервисы превращают баг в инцидент: отказ одного бьёт по соседям. Нужен дежурный, алерты, процедуры. Если падение чинят «утром придёт автор» — рано.
- Команд хватает? — Грубо говоря, нужна команда-владелец на сервис (или на кучку близких). Если одна команда тащит двадцать сервисов — это не автономия, это распределённый монолит на их плечах.
Эвристика: если на CI/CD, observability и владение данными ответ «нет» — дроби монолит на модули внутри одного процесса. Микросервисы — инструмент организационный, не технический. Окупаются, когда несколько команд начинают мешать друг другу в одном репозитории.
Must-know паттерны
Минимальный набор, который держишь в голове. Детали каждого — в основных секциях этого файла, здесь — карта, чтобы не забыть, что вообще существует.
Вход и маршрутизация:
- API Gateway — единая логическая точка входа, маршрутизация, cross-cutting (auth, rate limiting).
Точка входа логическая, не обязательно один физический инстанс. BFF — вариант: свой gateway под
каждый тип клиента (mobile/web).
- Service discovery — как клиент или роутер находит инстансы сервиса через service registry. Часто
это уже даёт платформа (DNS в Kubernetes), отдельный registry не всегда нужен.
Данные и согласованность:
- Saga — последовательность локальных транзакций с компенсациями вместо распределённой транзакции
(Garcia-Molina и Salem, 1987). Даёт eventual consistency, не ACID, и не изолирует промежуточные
состояния. Choreography (через события) или orchestration (явный координатор).
- Transactional outbox + CDC — атомарно пишешь данные и событие в одну БД, relay (например
Debezium) дочитывает outbox и публикует в broker. Лечит dual-write problem. Гарантия
at-least-once, не exactly-once.
- Idempotent consumer — обработка повтора даёт тот же результат. Обязательная пара к at-least-once:
дедупликацию делает потребитель, а не магия брокера.
- CQRS — раздельные write- и read-модели (Greg Young, ~2010). Не требует event sourcing и не обязан
жить в двух БД. Берёшь, когда чтения и записи реально расходятся по нагрузке и форме.
Устойчивость: - Timeout — верхняя граница ожидания ответа. Фундамент, на котором держатся остальные паттерны. Дефолтные (часто бесконечные) таймауты библиотек — частые грабли. - Retry с jitter — повтор с экспоненциальной задержкой и рандомом. Без jitter получаешь синхронный thundering herd. Только для идемпотентных операций, с retry budget. - Circuit breaker — перестаёт долбить упавшую зависимость, fail fast на cooldown (Michael Nygard, 2007). Смысл — STOP звонить, а не повторять. Не путай с retry. - Bulkhead — изоляция ресурсов (отдельные пулы на зависимость), чтобы один отказ не утопил весь сервис. Про blast radius, не про детект сбоя.
Эволюция и релизы:
- Contract testing — consumer-driven contracts (Ian Robinson, 2006; Pact — одна из реализаций).
Ловит ломающие изменения API до прода, дешевле полного end-to-end.
- Schema registry — версионирование и проверка совместимости схем событий (Avro/Protobuf).
Отдельный компонент (изначально Confluent), не часть Kafka.
- Strangler fig — постепенное обрастание легаси новым кодом с перехватом функций (Martin Fowler,
2004). Не big-bang.
- Canary / blue-green — canary льёт долю трафика на новую версию и смотрит метрики; blue-green
переключает весь трафик между двумя средами разом. Feature flags — отвязать релиз от деплоя.
Если коротко
Симптом или задача слева, паттерн справа. Не догма, а отправная точка — дальше думаешь по контексту.
| Симптом / задача | Паттерн |
|---|---|
| Клиент дёргает десяток сервисов напрямую, знает всю топологию | API Gateway, при разных клиентах — BFF |
| Mobile и web тянут из одного API разное, мешают друг другу | Backends for Frontends |
| Инстансы сервиса плавают по адресам, хардкодить IP нельзя | Service discovery (часто DNS платформы) |
| Бизнес-операция трогает несколько сервисов, 2PC не вариант | Saga (choreography или orchestration) |
| Надо атомарно сохранить данные и отправить событие | Transactional outbox + CDC |
| Сообщения иногда приходят дважды | Idempotent consumer |
| Сообщение не обработалось и теряется молча | Dead letter queue |
| Тяжёлые чтения душат write-нагрузку, формы запросов расходятся | CQRS, при join через сервисы — API composition |
| Зависимость тормозит и подвешивает твои потоки | Timeout (сначала), потом circuit breaker |
| Падающий сервис тянет за собой соседей | Circuit breaker + bulkhead |
| Ретраи синхронизируются и бьют волной | Retry с exponential backoff и jitter |
| Один шумный клиент выжирает мощность сервиса | Rate limiting (не путать с backpressure) |
| Быстрый producer заваливает медленного consumer | Backpressure |
| Боишься сломать потребителей при смене API | Consumer-driven contracts (Pact) |
| События меняют формат, нужна безопасная эволюция | Schema registry + Avro/Protobuf |
| Пилишь легаси-монолит, big-bang страшен | Strangler fig |
| Катишь рискованную версию, нужен быстрый откат | Canary (доля трафика) или blue-green (переключение) |
| Релиз и деплой надо развязать | Feature flags |
| Retry/mTLS/tracing хочется убрать из кода приложения | Service mesh (sidecar или ambient) |
| Сетевые отказы в проде ловишь только постфактум | Chaos engineering, контролируемо |
Источники¶
Канонические работы, на которые опираются паттерны из этого файла. Не учебный список — куда лезть за первоисточником, когда индустрия переврала смысл.
Доменные границы и декомпозиция
- Eric Evans — Domain-Driven Design (2003): откуда вообще bounded context и Anti-Corruption Layer. Bounded context — это граница модели в solution space, не синоним service и не синоним subdomain. ACL — защитный context-mapping слой, а не просто DTO-маппер.
- James Lewis & Martin Fowler — Microservices: a definition of this new architectural term (martinfowler.com, март 2014): каноническое описание стиля. Главное в нём — independent deployability и организация вокруг business capability, а не «маленький размер». «Micro» не про строки кода. Термин обсуждали и раньше (workshop ~2011) — Lewis и Fowler его не изобрели.
- Sam Newman — Building Microservices (O'Reilly, 2015) и Monolith to Microservices (2019): практика интеграции, разбиения монолита, BFF. Второй — целиком про то, как резать монолит постепенно.
Каталог паттернов
- Chris Richardson — microservices.io и Microservices Patterns (Manning, 2018): рабочая лошадка. Отсюда database per service, saga, API composition, transactional outbox, idempotent consumer, API gateway, service discovery. Каталог живой — у части паттернов нет жёсткого «года».
- Gregor Hohpe & Bobby Woolf — Enterprise Integration Patterns (Addison-Wesley, 2003): фундамент messaging. Dead letter channel, маршрутизация, паттерны обмена сообщениями. Старше микросервисов, но именно отсюда растут очереди и каналы, которыми они общаются.
Данные, транзакции, согласованность
- Hector Garcia-Molina & Kenneth Salem — Sagas, ACM SIGMOD (1987): первоисточник saga. Важно: изначально решала long-lived transactions внутри одной СУБД, не распределёнку. Даёт eventual consistency без изоляции, а не ACID-атомарность — индустрия часто это забывает.
- Greg Young — CQRS (~2010): разделение write-модели и read-модели. Строится на command-query separation Бертрана Мейера. CQRS не требует event sourcing и не обязан жить на двух базах — частое искажение.
- Jim Gray — Notes on Data Base Operating Systems (1978): формализация two-phase commit. 2PC даёт атомарность решения о коммите, но блокирует: упал координатор после yes-голосов — участники висят с захваченными locks. Поэтому в микросервисах берут saga, а не потому что 2PC «неправильный».
Устойчивость и отказы
- Michael Nygard — Release It! (Pragmatic Bookshelf, 2007): circuit breaker, bulkhead, timeout как
stability patterns. Circuit breaker — про то, чтобы перестать звонить упавшей зависимости, а не
про retry. Bulkhead — изоляция ресурсов, ограничение blast radius.
Hystrixпопуляризировал позже — но придумал Nygard. - Marc Brooker (AWS) — Exponential Backoff And Jitter (2015): почему к backoff нужен ещё и jitter. Без jitter retry синхронизируются и бьют залпом (thundering herd). Сам backoff старше — растёт из Ethernet/CSMA.
Эволюция контрактов
- Ian Robinson — Consumer-Driven Contracts: A Service Evolution Pattern (martinfowler.com, 2006):
ожидания потребителя как контракт, который ограничивает provider.
Pact— это одна реализация паттерна, а не сам паттерн. И это consumer-defined ожидания, не provider-defined спека.
Эксплуатация и развёртывание
- Adam Wiggins — The Twelve-Factor App (12factor.net, 2011): методология для cloud-portable SaaS. Не про контейнеры и не про микросервисы как таковые — старше контейнерной эры, но по делу.
- Martin Fowler — StranglerFigApplication (martinfowler.com, 2004): инкрементальная замена legacy. Новая система обрастает старую по краям, пока ту не выключишь. Не big-bang cutover — в этом весь смысл.
Топологию, выбор стиля и теорию sync-vs-async смотри в Macro-Architecture; детали протоколов — в Communication & API Style.
Главное правило: почти каждый паттерн здесь — это плата за сеть. Saga вместо ACID, circuit breaker вместо стектрейса, tracing вместо одного лога. Не бери микросервисы без причины (дефолт — modular monolith), а взяв — режь по bounded context, развязывай данные и проектируй под отказ, а не под happy path.