Монолит и микросервисы
Как делить систему, когда её делить и почему большинству команд стоит начинать с модульного монолита.
За 30 секунд
Монолит — это одно приложение, которое собирается и деплоится как единое целое. Микросервисы — множество маленьких приложений, каждое со своими данными, общающихся по сети. Модульный монолит — посередине: один деплой, но со строгими внутренними границами. Правильный выбор гораздо сильнее зависит от размера команды и ясности предметной области, чем от трафика.
Почему я это использую
- По умолчанию я использую модульный монолит: он даёт чёткие границы, не заставляя с первого дня платить налог микросервисов на сеть, деплой и observability.
- Большая часть боли, которую я вижу в молодых продуктах, идёт от запутанного кода, а не от нехватки сервисов. Хорошие границы модулей это лечат; разделение на сервисы — обычно нет.
- Выбор архитектуры — это ещё и бизнес-решение: микросервисы умножают расходы на хостинг, мониторинг и дежурства, и клиенты заслуживают услышать это до того, как за это заплатят.
- Когда разделение действительно нужно, хорошо проведённые границы модулей превращают его в механическое извлечение, а не в переписывание.
Как это работает
01Три формы: монолит, модульный монолит, микросервисы
Представьте ресторан. Монолит — одна кухня, где любой повар может схватить любой продукт с любой полки: поначалу быстро, потом хаотично. Модульный монолит — та же кухня, но разделённая на станции (гриль, кондитерская, салаты), у каждой свои полки и стойка, через которую передают заказы. Микросервисы — отдельные рестораны, которые отправляют друг другу заказы курьером: каждый может открыться, закрыться или уйти на ремонт независимо, но теперь каждая передача занимает время и может потеряться. В терминах софта монолит — это одна кодовая база, один тип процесса и обычно одна база данных. Модульный монолит сохраняет один деплой, но делит код на модули с публичными интерфейсами, и каждый модуль владеет своими таблицами. Микросервисы — независимо деплоящиеся процессы, у каждого своё хранилище данных, и общаются они через HTTP, gRPC или сообщения.
- Монолит: проще всего строить, тестировать, деплоить и отлаживать; риск — «большой ком грязи».
- Модульный монолит: один деплой, принудительные границы, вызовы внутри процесса, одна транзакция, когда она нужна.
- Микросервисы: независимые деплои и масштабирование каждого сервиса; цена — сложность распределённых систем.
02Реальная цена микросервисов
Микросервисы переносят сложность из кода в сеть. Вызов функции, который никогда не падал, становится запросом, который может упасть по таймауту, прийти дважды или выполниться так, что вы об этом не узнаете. Одна транзакция в базе превращается в распределённый процесс, которому нужны саги, outbox'ы и идемпотентность. Владение данными становится строгим: сервис заказов не может читать таблицу клиентов, значит, нужны API, события или локальные копии — а копии могут устаревать. Для отладки нужны распределённый трейсинг, связанные логи и дашборды для каждого сервиса, иначе одно медленное оформление заказа превращается в детективную историю. Для деплоя нужны CI-пайплайны, версионированные контракты, service discovery и обычно оркестратор. И каждому сервису нужны свой мониторинг, алерты, секреты и обновления. Ничего из этого не невозможно, но это постоянный налог, который вы платите каждую неделю.
- Распределённые транзакции: нет общей ACID-транзакции между сервисами.
- Сбои сети: таймауты, повторы, частичные отказы, каскадные падения.
- Владение данными: никаких join'ов между сервисами; дублированные данные с eventual consistency.
- Observability и стоимость: трейсинг, больше инфраструктуры, больше зона ответственности дежурных.
03Поиск границ через bounded context'ы
Самое сложное в любом разделении — где резать. Domain-Driven Design даёт практичный инструмент: bounded context — область бизнеса, где слова имеют одно точное значение. «Товар» в каталоге — это описание с фотографиями; на складе — это остатки; в биллинге — строка с ценой. Каждый контекст получает свою модель и свой модуль. У хороших границ высокая связность внутри (что меняется вместе, живёт вместе) и слабое зацепление снаружи (узкий стабильный интерфейс). Полезный тест: может ли команда неделю менять этот модуль, ни с кем не согласовываясь? Если нет — граница проведена неправильно. Режьте по бизнес-возможностям (заказы, платежи, лояльность), а не по техническим слоям («сервис базы данных» или «сервис валидации»): это только порождает болтливые и хрупкие зависимости.
// eslint.config.mjs: modules may import each other only through a public index
export default [
{
files: ["src/modules/**/*.ts"],
rules: {
"no-restricted-imports": ["error", {
patterns: [{
group: ["@/modules/*/internal/*", "@/modules/*/db/*"],
message: "Use the other module's public index.ts, not its internals.",
}],
}],
},
},
];
04Как построить модульный монолит, который останется модульным
Границы, которые существуют только на диаграмме, размываются за несколько месяцев. Закрепляйте их в коде. Дайте каждому модулю папку с публичной точкой входа (сервисы, DTO, события), а всё остальное держите внутренним. Пусть каждый модуль владеет своими таблицами, и запретите другим модулям обращаться к ним напрямую; чтение между модулями идёт через сервис модуля-владельца. Используйте доменные события внутри процесса для побочных эффектов («заказ оплачен» начисляет баллы лояльности), чтобы модули не вызывали друг друга длинными цепочками. Добавьте правила линтера или архитектурные тесты, которые роняют сборку при запрещённых импортах. Во фреймворках с системой модулей, например в NestJS, экспортируйте только то, что можно использовать другим модулям. Награда: когда одному модулю действительно понадобится отдельное масштабирование или деплой, у вас уже есть его интерфейс, его данные и его события, и извлечение становится проектом на недели, а не переписыванием.
05Миграция по паттерну strangler fig
Когда существующий монолит нужно разделить, никогда не пытайтесь переписать всё разом: это замораживает фичи на месяцы и обычно проваливается. Используйте паттерн strangler fig, названный в честь фикуса-душителя, который медленно обрастает вокруг дерева. Поставьте перед монолитом слой маршрутизации (reverse proxy или API gateway). Выберите одну хорошо очерченную возможность, постройте её как новый сервис и направьте туда только её эндпоинты, а всё остальное по-прежнему идёт в монолит. На время перехода синхронизируйте данные через события или change data capture, а затем передайте владение данными новому сервису. Повторяйте по одной возможности за раз и удаляйте старый код после каждого шага. Каждый шаг маленький, обратимый и готовый к выкатке, так что бизнес продолжает двигаться, пока архитектура меняется под ним.
- Начинайте с возможности, у которой ясное владение данными и мало входящих зависимостей.
- Запускайте старый и новый пути параллельно и сравнивайте результаты, прежде чем переключать трафик.
- Измеряйте: если новый сервис не упрощает деплой или масштабирование — остановитесь.
06Как выбрать: размер команды, предметная область и потребности в масштабировании
Закон Конвея гласит, что системы повторяют структуру коммуникаций команд, которые их строят. Одна-две небольшие команды почти ничего не выигрывают от микросервисов — они и так легко договариваются, — а вот эксплуатационную цену платят полностью. Микросервисы начинают окупаться, когда несколько автономных команд блокируют друг друга на общем релизе, когда у частей системы очень разные требования к масштабированию или надёжности (видеоэнкодер против админки) или когда требования регуляторов диктуют жёсткую изоляцию. Сам по себе трафик редко бывает причиной: хорошо построенный монолит горизонтально масштабируется за load balancer'ом до очень больших нагрузок. Разумный вариант по умолчанию для большинства компаний — начать с модульного монолита, вкладываться в границы, тесты и observability и выделять сервис только тогда, когда это оправдывает конкретная, измеренная боль.
Когда применять
- Модульный монолит: новые продукты, команды до нескольких десятков инженеров или предметные области, которые ещё изучаются.
- Микросервисы: несколько независимых команд, которые блокируются общим циклом релизов.
- Микросервисы: компоненты с очень разными требованиями к масштабированию, рантайму или надёжности.
- Выделение одного сервиса: горячий путь (поиск, обработка медиа), которому нужно отдельное масштабирование.
Когда не стоит
- Не выбирайте микросервисы, чтобы выглядеть современно или потому что их использует крупная техкомпания.
- Не делите, пока границы ещё неясны: неправильные границы через сеть обходятся гораздо дороже.
- Не делите продукт маленькой команды на большее число сервисов, чем в ней инженеров.
Альтернативы
- Обычный монолит
Хорош для небольших приложений и прототипов; добавьте границы модулей, когда кодовая база вырастет.
- Модульный монолит
Один деплой с принудительными внутренними границами; лучший вариант по умолчанию для большинства команд.
- Service-based архитектура
Несколько крупных сервисов (например, основное приложение, поиск, медиа) вместо десятков крошечных.
- Serverless-функции
Хороши для «рваных» задач по событиям; сложнее для длинных процессов и локальной отладки.
Лучшие практики
- Проводите границы по бизнес-возможностям с помощью bounded context'ов, а не по техническим слоям.
- Давайте каждому модулю или сервису исключительное владение своими данными.
- Закрепляйте границы автоматически — правилами линтера или архитектурными тестами.
- Передавайте побочные эффекты через события, а не через длинные цепочки синхронных вызовов.
- Вложитесь в логи, метрики и трейсинг, прежде чем что-либо делить.
- Выделяйте сервисы по одному с помощью паттерна strangler fig и сохраняйте обратимость каждого шага.
Частые ошибки
- Построить «распределённый монолит»: сервисы, которые должны деплоиться вместе и делят одну базу.
- Разделить слишком рано, ещё не поняв предметную область, а потом платить за обратное слияние сервисов.
- Строить цепочки синхронных вызовов (A вызывает B, B вызывает C), так что один медленный сервис роняет всё.
- Забывать, что каждый сервис умножает работу по CI, мониторингу, секретам и дежурствам.
Глубже
07Связанность, которую не видно: временная, по данным и по деплою
Правила импортов ловят связанность в коде, но успех разделения решают три более тихих вида. Временная связанность: сервис A может завершить запрос, только пока B работает, потому что вызывает его синхронно. Тогда доступность перемножается: три сервиса по 99.9 процента в цепочке дают для всего потока примерно 99.7 процента. Связанность по данным: два модуля читают или пишут одни и те же таблицы, поэтому изменение схемы в одном ломает другой без единого импорта между ними. Связанность по деплою: изменение требует согласованных релизов, потому что контракт поменялся без обратной совместимости. Измерьте всё это до разделения. Посмотрите, каких таблиц касается каждый модуль, на граф вызовов из трейсинга и на то, какие папки меняются в одних и тех же коммитах за несколько месяцев (change coupling по истории Git). Модули, которые постоянно меняются вместе, должны жить вместе, что бы ни говорила диаграмма.
- Доступность цепочки: перемножьте SLO каждого синхронного шага.
- Общие таблицы нарушают границу даже при чистых импортах.
- Анализ совместных изменений в Git дёшево показывает реальную связанность.
08Контракты между сервисами: версионирование и consumer-driven тесты
Как только граница становится сетевой, её интерфейс превращается в опубликованный контракт, и независимый деплой зависит от его стабильности. Следуйте правилу tolerant reader: потребители игнорируют незнакомые поля, а производители только добавляют необязательные. Ломающие изменения идут через expand and contract: добавьте новое поле или эндпоинт, переведите потребителей, затем удалите старое, когда его использование упадёт до нуля. Consumer-driven contract тесты (например, с Pact) позволяют каждому потребителю зафиксировать запросы и поля ответа, на которые он опирается; CI провайдера проигрывает эти ожидания и падает до того, как ломающее изменение уйдёт в прод, без подъёма всей системы. Это заменяет медленные и нестабильные end-to-end окружения в роли основной страховки. Так же версионируйте события и закрепите за каждым API письменно указанного владельца, чтобы вопросам было куда идти.
09Механика выделения: branch by abstraction и anti-corruption layer
Внутри монолита branch by abstraction готовит модуль к выделению без долгоживущей ветки. Введите интерфейс для этой возможности, проведите через него всех вызывающих, затем добавьте вторую реализацию, которая ходит в новый сервис и включается feature flag по тенанту или по проценту. Двигайтесь постепенно и держите in-process реализацию как fallback, пока удалённая не докажет свою надёжность. Если у нового сервиса модель чище, чем у legacy-кода, поставьте между ними anti-corruption layer: переводчик, который отображает старые понятия на новые, чтобы legacy-названия и причуды не протекали в новый дизайн. Данные переезжают поэтапно: сначала новый сервис читает реплицированную копию, затем становится писателем, а монолит читает из него, и в конце старые таблицы удаляются.
// Branch by abstraction: callers depend on the interface, not the location
export interface PricingPort {
quote(cartId: string): Promise<Quote>;
}
export function pricingFor(tenantId: string, deps: Deps): PricingPort {
return deps.flags.isOn("pricing-service", tenantId)
? new RemotePricing(deps.http) // new service, behind timeouts and retries
: new InProcessPricing(deps.db); // existing module, kept as fallback
}
10Топология команд и inverse Conway manoeuvre
Если системы повторяют структуру общения команд, можно спроектировать организацию так, чтобы получить нужную архитектуру: это inverse Conway manoeuvre. Дайте каждой stream-aligned команде сквозное владение одним или несколькими bounded context, включая данные, деплои и on-call. Платформенная команда даёт «мощёные дороги» (шаблоны CI, observability, scaffolding сервисов), чтобы каждый новый сервис не изобретал их заново. Следите за когнитивной нагрузкой: команда, владеющая большим числом сервисов, чем способна удержать в голове, начнёт срезать углы во всех. Грубая проверка — отношение деплоимых единиц к инженерам: когда оно превышает единицу, эксплуатация начинает вытеснять работу над фичами. Перерисовывайте границы при изменении структуры команд: сервис, которым владеют три команды, становится узким местом координации, а бесхозный тихо гниёт.
Реальные ситуации
Команда из пяти человек хочет переписать всё на микросервисы
Проблема
Продукт, который строили четыре года, стало трудно менять: каждая фича задевает множество файлов, а релизы часто ломают несвязанные экраны. Команда предлагает переписать его в виде двенадцати микросервисов.
Как решить
Возразите против переписывания и сначала устраните связанность. На коротком воркшопе с командой нарисуйте карту bounded context, затем реорганизуйте код в модули с публичными точками входа и lint-правилами, запрещающими глубокие импорты. Распределите владение таблицами по модулям и замените межмодульные запросы вызовами сервисов или событиями. Добавьте трейсинг и characterisation-тесты на самые рискованные сценарии. Через несколько месяцев измерьте частоту деплоев, долю неудачных изменений и то, скольких модулей касается типичная фича. Только если какому-то модулю всё ещё нужно отдельное масштабирование или свой ритм релизов, выделите этот один модуль по паттерну strangler fig.
После разделения latency checkout выросла вдвое
Проблема
После выделения pricing, inventory и promotions в сервисы p95 latency checkout выросла с 300 ms до 700 ms, а ошибки резко подскакивают при каждом деплое inventory.
Как решить
Протрейсьте один checkout от начала до конца — обычно найдутся последовательные синхронные вызовы, каждый со своими ретраями. Распараллельте независимые вызовы, поставьте жёсткие таймауты ниже собственного бюджета вызывающего и ретрайте только идемпотентные вызовы с backoff и jitter. Для медленно меняющихся данных (цены, правила акций) держите локальную read model, обновляемую событиями, вместо запроса на каждый вызов. Добавьте circuit breaker, чтобы деплой inventory не валил checkout, а деградировал до «наличие подтвердим позже». Используйте rolling-деплои с readiness-проверками. Если два сервиса всегда вызываются вместе, слейте их обратно.
Выделение тяжёлого модуля обработки медиа
Проблема
Обработка изображений и видео в монолите съедает CPU во время загрузок и замедляет API для всех. Это единственная часть с сильно отличающимися требованиями к ресурсам.
Как решить
Это хороший кандидат на выделение: узкий интерфейс, ясное владение данными и другой профиль масштабирования. Сначала определите контракт: API сохраняет оригинал в object storage и публикует задачу «media uploaded» с ID и ключом хранилища. Отдельный worker-сервис, масштабируемый по глубине очереди, обрабатывает её и отправляет «media processed» с вариантами. API идемпотентно обновляет свои записи по этому событию. Выкатите это за флагом, прогоните оба пути на выборке, сравните результаты, затем полностью переключитесь и удалите in-process код.
Шпаргалка
- Запрет глубоких импортов между модулями
ESLint no-restricted-imports on '@/modules/*/internal/*'- Найти модули, которые меняются вместе
git log --name-only --since=6.months --format='' | sort | uniq -c | sort -rn- Доступность синхронной цепочки
0.999 x 0.999 x 0.999 ≈ 0.997 (около 2 ч простоя в месяц)- Безопасное изменение контракта
Expand (добавить optional) -> перевести потребителей -> contract (удалить)- Consumer-driven contract тестирование
Pact: потребитель публикует pact, CI провайдера его проверяет- Переключатель постепенного выделения
Feature flag по тенанту: in-process или удалённая реализация- Strangler-маршрутизация на краю
location /api/billing/ { proxy_pass http://billing; }- Проверка здравого смысла
Деплоимых единиц на инженера меньше 1; выше — эксплуатация доминирует
В моих реальных проектах
- Multi-tenant POS SaaS, которым я руковожу вместе с коллегой, — монорепозиторий на pnpm + Turborepo: один NestJS API плюс воркер, разделённые на 83 доменных модуля, а рядом 6 приложений и 10 общих пакетов.
- Побочные эффекты между модулями там идут через transactional outbox в очереди BullMQ и обработчики, а не через прямые вызовы модулей друг другом.
- На платформе заказов для ресторанов один Laravel API обслуживает веб, мобильные приложения и админку, а контроллеры разделены на Shared/Ui/Mobile/Admin, так что каждый канал переиспользует одну и ту же базовую логику.
- На платформе зоотоваров около 90 моделей живут в 12 доменных папках примерно со 110 сервисными классами, и это сохраняет монолит удобным для навигации.
Вопросы и ответы
Q1Когда вы порекомендуете клиенту микросервисы?
Когда есть конкретная боль, которую решает только независимый деплой: несколько команд блокируют друг друга на одном релизном поезде, есть компонент с совсем другими требованиями к масштабированию или рантайму, или регуляторы требуют жёсткой изоляции. Я бы также проверил, может ли клиент позволить себе платформенную работу: CI на каждый сервис, трейсинг, дежурства, версионирование контрактов. Если команда маленькая или предметная область ещё меняется, я рекомендую модульный монолит со строгими границами: он даёт большую часть организационной пользы за малую долю стоимости, а позже его можно разделить по тем же линиям.
Q2Что такое распределённый монолит и как его избежать?
Это система, задеплоенная как множество сервисов, которая всё равно ведёт себя как одно целое: сервисы делят базу, должны выпускаться вместе или вызывают друг друга синхронно длинными цепочками, так что один сбой ломает всё. Вы получаете издержки обоих миров и выгоды ни одного. Избежать этого помогают исключительное владение данными у каждого сервиса, передача побочных эффектов через события, версионирование контрактов, чтобы сервисы деплоились независимо, и границы вокруг бизнес-возможностей, а не технических слоёв. Простая проверка: если изменение одной фичи регулярно требует согласованного деплоя трёх сервисов, границы проведены неправильно.
Q3Как вы определяете границы сервисов или модулей?
Я начинаю с бизнеса, а не с кода. С помощью Domain-Driven Design ищу bounded context'ы — области, где термин вроде «заказ» или «товар» имеет одно точное значение и понятного владельца. Затем проверяю связность и зацепление: что меняется вместе, должно жить вместе, а интерфейс между областями должен быть узким и стабильным. Самый сильный сигнал — владение данными: если двум кандидатам постоянно нужны таблицы друг друга, скорее всего, это один контекст. Сначала я проверяю границы внутри модульного монолита, где переносить их дёшево, и только потом превращаю какие-то из них в сетевые границы.
Q4Как бы вы мигрировали легаси-монолит на сервисы?
Постепенно, по паттерну strangler fig. Сначала добавляю observability и тесты вокруг монолита, затем ставлю перед ним слой маршрутизации. Выбираю одну возможность с ясными границами и малым числом зависимостей, строю её как новый сервис, синхронизирую данные через события или change data capture и переключаю её маршруты — в идеале после того, как оба пути поработали параллельно и результаты сравнили. Когда всё стабильно, новый сервис становится владельцем этих данных, а старый код удаляется. Каждый шаг маленький и обратимый. Переписывание всего разом останавливает работу над фичами на месяцы и редко доходит до конца.
Q5Как вы обрабатываете транзакцию, которая охватывает два сервиса?
Вы принимаете, что общей ACID-транзакции нет, и проектируете с учётом этого. Каждый сервис коммитит свою локальную транзакцию и публикует событие через transactional outbox, так что изменение состояния и сообщение сохраняются атомарно. Процесс координируется как сага — либо оркестрируется одним компонентом, либо хореографируется через события, — с компенсирующим действием для каждого шага, например возвратом платежа, если резервирование товара не удалось. Потребители должны быть идемпотентными, потому что сообщения могут приходить больше одного раза. Если процессу это нужно постоянно, это намёк, что два сервиса должны быть одним.
Q6Масштабируется ли монолит?
Да, и гораздо дальше, чем принято думать. Stateless-монолит запускается как множество одинаковых инстансов за load balancer'ом, с сессиями в Redis, файлами в объектном хранилище и базой с read-репликами и кэшированием. Многие очень крупные продукты годами работали на монолитах. Чего монолит не даёт — так это независимого масштабирования одного горячего компонента или независимого деплоя отдельными командами. Если одной части, например обработке изображений или поиску, нужны другие ресурсы, выделите только её. Масштабирование обычно становится проблемой базы данных и кэширования задолго до того, как станет проблемой архитектуры.
Вопросы уровня senior
S1Как сохранить данные согласованными и доступными для запросов, когда у каждого сервиса своя база?
Каждый сервис — единственный писатель своих данных, а остальные получают их одним из трёх способов. Для свежих чтений, критичных для решения, вызывайте API владельца с таймаутом и fallback. Для частых чтений медленно меняющихся данных держите локальную read model, обновляемую событиями владельца, принимая отставание в несколько секунд. Для отчётности по нескольким сервисам стримьте изменения в отдельное аналитическое хранилище вместо join живых баз. Межсервисные записи становятся сагами с компенсациями и событиями, публикуемыми через outbox. Компромисс явный: вы получаете независимый деплой и масштабирование, а платите eventual consistency, дублированием данных и большим объёмом кода. Если фиче постоянно нужны атомарные записи в два сервиса, это признак того, что граница проведена неверно.
S2Какие метрики покажут, что изменение архитектуры действительно помогло?
За основу беру четыре метрики DORA: частоту деплоев, lead time изменений, долю неудачных изменений и время восстановления сервиса. Хорошее разделение должно повысить частоту и сократить lead time для вовлечённых команд, не увеличивая долю неудачных изменений. Добавляю сигналы, специфичные для архитектуры: скольких модулей или сервисов касается типичная фича, как часто деплои требуют координации, p95 latency и error rate ключевых пользовательских сценариев, а также стоимость инфраструктуры и on-call на команду. Измеряйте несколько месяцев до и после. Если деплои не стали независимее, а инцидентов стало больше, изменение добавило издержек без пользы, и слить сервисы обратно — законное, профессиональное решение.
S3Синхронные вызовы или события между сервисами: как вы решаете для каждого взаимодействия?
Я спрашиваю, нужен ли вызывающему ответ, чтобы продолжить. Запросы, которые определяют следующий экран пользователя, например цена или проверка авторизации, синхронные и защищены таймаутами, ретраями для идемпотентных операций и circuit breaker. Побочные эффекты, которые могут произойти после ответа, например отправка чеков, обновление loyalty или переиндексация поиска, — это события: они убирают временную связанность и позволяют потребителям быть недоступными, не вредя производителю. Команды конкретному сервису, которые могут выполняться долго, идут через очередь со статусом, который клиент может опрашивать. Цена событий — более сложная отладка и eventual consistency, поэтому, прежде чем сильно на них опираться, я требую correlation ID, трейсинг через очереди и мониторинг dead-letter.
Глоссарий
- Bounded context
- Часть домена, где каждый термин имеет одно точное значение и одну модель.
- Распределённый монолит
- Множество сервисов, которые всё равно приходится деплоить и менять вместе, сочетая издержки обоих стилей.
- Strangler fig
- Замена legacy-системы по частям через постепенное перенаправление трафика на новые компоненты.
- Branch by abstraction
- Введение интерфейса, чтобы новая реализация заменила старую за переключателем.
- Anti-corruption layer
- Переводчик, который не даёт понятиям legacy-модели протекать в новый дизайн.
- Закон Конвея
- Системы склонны повторять структуру коммуникаций организации, которая их строит.
- Tolerant reader
- Потребитель, игнорирующий незнакомые поля, чтобы производители могли добавлять данные, не ломая его.
- Consumer-driven contract
- Тесты, в которых потребители объявляют, на что опираются, а провайдер проверяет это в CI.
- Временная связанность
- Когда один компонент может работать, только если другой доступен в тот же момент.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.