В этой статье я хотел бы рассмотреть микросервисные паттерны под другим углом. Когда я начинал изучение микросервисных паттернов, у меня постоянно был вопрос: Так это же было в другом паттерне. Я решил немного структурировать их: объединить по похожим элементам. Кластеризировать микросервисные паттерны достаточно тяжело так как каждый паттерн по‑своему уникален, однако для запоминания на собеседованиях или для себя это сделать можно. Основной контент статьи — картинка, далее идёт описание, чтобы всё было в одном месте.
Эта статья предназначена для более быстрого запоминания/повторения паттернов микросервисов. Кое‑где я приводил кейсы, которые могут быть непонятны новичкам. Здесь нет подробных кейсов применения каждого паттерна так как иначе статья получилась бы на другую тему. Для удобства я приложил ссылки — чтобы избежать дублей.

1. DDD — Bounded Context
Описание
Паттерн, который определяет чёткие границы модели и ответственности внутри микросервиса. Каждый сервис содержит свою доменную модель и не обязан соответствовать моделям других сервисов. Один и тот же бизнес-термин в разных контекстах представляет разные сущности с уникальными атрибутами и поведением.
Пример: «Ресурс»
В сервисе планирования — это сотрудник (skills, calendar).
В сервисе API-шлюза — это endpoint (uri, method).
В сервисе инфраструктуры — это сервер (CPU, RAM).
Преимущество: Такое разделение позволяет каждому сервису развивать свою модель, не координируя каждое изменение со всеми остальными, что увеличивает скорость разработки и отказоустойчивость системы.
Эволюция системы: от монолитного хаоса к DDD-порядку
Ситуация «ДО»: Монолит e-commerce-платформы «MegaShop»
В системе одна большая, запутанная модель данных. Все таблицы в общей БД, все модули знают друг о друге.
Единая «божественная» сущность Product (в коде):
java
@Entity
public class Product {
private Long id;
private String name;
private String description;
// Для каталога
private BigDecimal price;
private String category;
private String[] images;
private Double rating;
// Для склада
private Integer quantityInStock;
private String warehouseLocation;
private String batchNumber;
private Date expiryDate;
// Для доставки
private Double weight;
private String dimensions;
private Boolean isFragile;
// Для маркетинга
private String seoKeywords;
private String promoText;
private Boolean isFeatured;
// 50+ методов, отвечающих за всё:
public void calculateDeliveryCost() {...}
public void applyDiscount() {...}
public void reserveFromStock() {...}
public void generateSeoUrl() {...}
public void checkExpiry() {...}
}Проблемы монолита:
Конфликты команд: Команда маркетинга хочет добавить поле videoReview, а команда логистики против — оно им не нужно. Расползание агрегата - усложнение ответа без пользы другой команды.
Медленная разработка: Любое изменение требует полного регресса (везде ли всё поменяли) и согласования со всеми.
Масштабирование невозможно: При пиковой нагрузке на распродажах «падает» весь монолит, включая складской учёт т.к. передаётся слишком большой объём данных из-за разросшегося агрегата.
Когнитивная перегрузка: Новый разработчик должен понять все 50+ полей и методов, даже если работает только над каталогом.
Решение «ПОСЛЕ»: Разбиение по DDD и ограниченным контекстам
Мы выделяем 3 основных ограниченных контекста и реализуем их как отдельные микросервисы, чтобы разделить ответственность

Пример полей в микросервисе команды каталога:
Ответственность: Показ и поиск товаров, ценообразование, рейтинги.
java
// Сервис: catalog-service
// База данных: catalog_db
@Entity
public class CatalogProduct {
private ProductId id; // Уникальный ID контекста
private String name;
private String description;
private Money price; // Value Object для денег
private String category;
private String[] imageUrls;
private Double rating;
private Boolean isActive; // Активен для продаж
private String seoSlug;
// Только методы каталога
public void changePrice(Money newPrice) {...}
public void updateRating(Double newRating) {...}
public void deactivate() { this.isActive = false; }
}Командна каталога решила Проблемы монолита:
Хочет добавить новые поля - ни с кем не надо согласовывать т.к. увеличит объём обмена данными только для микросервиса команды каталога
Регресс незначим т.к. микросервисы намного проще, разработка быстрая
Маштабирование микросервиса (поднятие новых под) намного дешевле, чем для всего монолита, можно точечно увеличить ресурсы конкретного отдела
Когнитивной перегрузки нет, см. п. 2.
Когда применять
Большая сложная предметная область (банки, страхование, e-commerce)
Требуется долгосрочная поддерживаемость
Команды работают независимо, нужны границы контекстов
Много связанных сущностей, которые нужно «развязать»
⚠️ Когда НЕ применять
В маленьких проектах (простая CRUD-система)
Если команды маленькие (1–2 человека)
Если предметная область банально проста.
Заблуждения
«DDD — это про микросервисы»
✔ Нет, DDD может быть и в монолите. (Проще потом разбивать)«Bounded Context = таблицы в БД»
✔ Нет, это предметная модель, не физические таблицы.«Один bounded context = один сервис»
✔ Не обязательно, иногда один BC = несколько сервисов.
🔗 Связи с другими паттернами
Использует:
ACL
DTO
Mapper
Усиливает:
Database per Service
CQRS
Альтернатива:
Разбиение по слоям (анти-паттерн)
Разбиение по бизнес-возможностям (Business Capability)
Описание
Паттерн, который делит систему на крупные куски по тому, что делает бизнес:
Кредиты, Платежи, Карты, Идентификация, Профиль клиента, Антифрод и т.д.
Это уровень выше DDD:
Business capability = зона ответственности бизнеса («кредитование», «платежи»).
Bounded Context (DDD) = как внутри этой зоны устроена доменная модель.
Мини-пример (в отличии от DDD)
Есть интернет-банк:
Capability «Кредитование» (Loans)
Capability «Платежи» (Payments)
Capability «Профиль клиента» (Client Profile)
Внутри capability «Кредитование» могут быть несколько bounded context’ов:
Контекст «Оформление кредита» (application, скоринг)
Контекст «Обслуживание кредита» (график, погашения)
Контекст «Реструктуризация»
То есть:
Разбиение по бизнес-возможностям → мы решили, что есть большая зона «Кредитование».
DDD / Bounded Context → мы внутри «Кредитования» дальше делим модель на понятные контексты.
Частые заблуждения
«Business capability = микросервис»
❌ Нет. Capability — крупная область (Loans),
внутри неё может быть много сервисов: loan-offer-service, loan-contract-service, loan-schedule-service.«Это то же самое, что DDD / Bounded Context»
❌ Нет.Capability — организационно-бизнесовый уровень («чем занимается команда/юнит»).
Bounded Context — архитектурный уровень модели внутри этой области.
Обычно: одна capability → несколько bounded context’ов.
«Разберёмся с capability потом, главное — код»
❌ Наоборот. Нормальные границы микросервисов почти всегда следуют за границами capability.
Если сверху хаос — снизу тоже будет хаос.
🔗 Связи с другими паттернами
Использует / опирается на:
DDD / Bounded Context — для внутренней структуры модели.
ACL — для аккуратного взаимодействия между capability, чтобы чужая модель не «заразила» твою.
DTO / Mapper — для адаптации данных между capability.
Усиливает:
Database per Service — каждая capability владеет «своими» данными.
BFF — API получаются естественными: loans/*, payments/*, profile/*.
API Gateway — удобная маршрутизация по capability.
Часто комбинируется с:
Strangler — когда из монолита выкусывают куски, часто первым шагом выделяют capability.
Saga — бизнес-процессы обычно проходят через несколько capability.
Шаблон “Душитель”
Нужен, чтобы постепенно выпиливать из монолита микросервисы по предыдущим 2ум шаблонам.
🔗 Связи с другими паттернами
DDD / Bounded Context — для внутренней структуры модели.
Шаблон “ACL” (Anti-Corruption Layer)
Продвинутый адаптер, помимо тупых маппингов и преобразования форматов (SOAP <-> REST)
Оберегает бизнес-инварианты (правила, которые нельзя нарушить). Например, в legacy-системе активный клиент это пользователь, заходивший не меньше месяца назад. В нашей АС это пользователь, заходивший не меньше месяца назад, прошедший верификацию KYC и не в чёрном списке. ACL собирает эту информацию с legacy и других систем, поэтому до конца адаптером его назвать нельзя.
Преобразует коды ответов:
Легаси сервис возвращает:
ERR_102 —«паспорт неверный»
ERR_219 — «истек срок сессии»
ERR_500 — «внутренняя ошибка»
А твой новый сервис должен возвращать единый контракт:
validation_error (422)
business_error (409)
technical_error (503)
Итог: Нет ACL, система поставщик сменится - будешь переписывать все бизес-правила
🔗 Связи с другими паттернами
Использует:
ACL
DTO
Mapper
Circuit Breaker
Частые заблуждения
«ACL — это просто маппинг DTO»
✔ Нет.
Mapper перекладывает поля.
ACL переводит смысл, фильтрует правила и защищает домен.
«ACL = Adapter»
✔ Частично.
ACL часто реализуется через Adapter / Gateway,
но его цель — не совместить интерфейсы, а изолировать домен.
❌ «ACL — это overengineering»
✔ Пока легаси не меняется — кажется так.
При смене интегратора ACL экономит месяцы переписывания.
Data Transfer Object (DTO)
Описание
DTO (Data Transfer Object) — это объект, предназначенный исключительно для передачи данных между слоями или сервисами.
Он не содержит бизнес-логики, инвариантов и поведения — только структуру данных, удобную и безопасную для обмена.
Ключевая идея DTO:
доменная модель ≠ контракт обмена данными
Пример: почему нельзя отдавать домен напрямую
❌ Плохой вариант (без DTO)
@Entity
class LoanContract {
ContractId id;
Money debt;
InterestRate rate;
PaymentSchedule schedule;
boolean isOverdue;
void applyPayment(Payment p) { ... }
}И ты возвращаешь его прямо в API.
Проблемы:
фронт видит внутренние поля
любое изменение домена = breaking change API
появляются «временные костыли» под UI
домен начинает подстраиваться под внешний контракт
✅ Хороший вариант (с DTO)
class LoanStatusDto {
String contractId;
BigDecimal debt;
BigDecimal nextPaymentAmount;
LocalDate nextPaymentDate;
boolean overdue;
}DTO содержит ровно то, что нужно клиенту
доменная модель свободно эволюционирует
Outbox Pattern (Transactional Outbox)
Описание
Outbox Pattern — это паттерн, который гарантирует, что изменение данных в БД и публикация события во внешний брокер происходят согласованно.
Проблема:
Если сервис сначала пишет данные в БД, а потом отправляет сообщение в брокер, между этими операциями возможен сбой.
В результате данные сохранены, а событие потеряно, либо событие отправлено несколько раз.
Решение (Outbox Pattern):
Перед отправкой сообщение сохраняется в outbox-таблицу в той же транзакции, что и бизнес-данные.
BEGIN TRANSACTION
1. Обновили бизнес-данные
2. Записали событие в outbox
COMMIT
Отдельный процесс читает outbox и публикует события в брокер.
После получения подтверждения от брокера сообщение помечается как отправленное.
Сбои и перезапуски не приводят к потере событий, а возможные дубли обрабатываются на стороне consumer’ов.
Вот подробная статья по Outbox
Частые заблуждения
❌ «Outbox = Kafka exactly-once»
✔ Нет.
Outbox даёт at-least-once delivery.
Дедупликация — ответственность consumer’а.
❌ «Outbox — это просто очередь в БД»
✔ Нет.
Outbox — часть транзакционного контура домена.
🔗 Связи с другими паттернами
Часто используется вместе с:
Event-Driven Architecture
Saga — шаги саги публикуются через outbox
Shared Database (Разделяемая база данных) - АНТИПАТТЕРН
📌 Описание
У нас есть несколько отдельных сервисов, которые хранят свои данные в одной БД и читают из неё.
На уровне данных они жёстко связаны:
читают и пишут в одни таблицы,
зависят от одной схемы,
знают структуру данных друг друга.
Ключевая проблема:
границы сервисов размываются на уровне данных, даже если на уровне API они существуют.
Зачем так делают (почему он вообще существует)
Shared Database почти никогда не выбирают осознанно как «хороший патт��рн».
Он появляется по причинам:
исторический монолит с общей БД
миграция к микросервисам «по кускам»
давление сроков («потом разнесём»)
отчётность / DWH / legacy-интеграции
отсутствие доверия к событиям / асинхронности
Важно:
Shared Database — это чаще переходное состояние, а не целевая архитектура.
Event Sourcing (Хранение состояния через события)
Описание
Без Event Sourcing (как обычно): Есть БД счёта с полями: consumer_id, balance (количество денег на счёте), last_entrance (время последнего входа)
С Event Sourcing другая модель хранения: consumer_id, balance_changed (операция, которая прошла по счёту), date, …. Здесь я при создании инициализирую баланс 0 и потом подсчитываю по транзакциям: 0 +(денег пришло) — (денег ушло).
Зачем нужен Event Sourcing
Event Sourcing применяют, когда важно:
полная история изменений (аудит, регуляторика). Например статусная модель документа для ЭДО.
восстановление состояния на любой момент времени
пересчёт данных при изменении логики
сложные бизнес‑инварианты
детальное отслеживание бизнес‑фактов
Почему бы просто не хранить историю изменений? История изменений (лог‑история) — это просто изменения, произошедшие с элементом, а Event‑sourcing — применение бизнес‑правила, которое строго определено в коде. Событие «Спиши 200Р со счёта» создастся только, если достаточно средств (отработала функция проверки и произошло списание), а лог‑история лишь отразил бы: было столько — то, стало столько‑то.
Частые заблуждения
❌ «Event Sourcing = Event-Driven Architecture»
✔ Нет.
Event Sourcing — про хранение данных.
EDA — про взаимодействие сервисов.
❌ «Event Sourcing = CQRS»
✔ Нет.
CQRS часто используют вместе с Event Sourcing,
но можно применять и по отдельности.
❌ «Event Sourcing — это лог изменений»
✔ Нет.
События — это доменные факты, а не технические логи.
❌ «События можно менять»
✔ Нет.
События иммутабельны.
Ошибки исправляются новыми событиями.
Пример доменного агрегата
class Account {
Money balance = Money.zero();
void apply(MoneyDeposited event) {
balance = balance.plus(event.amount());
}
void apply(MoneyWithdrawn event) {
balance = balance.minus(event.amount());
}
}Пример Event Store
event_store
------------
event_id (Id конкретного события по агрегату - uuid)
aggregate_id (Id изменений конкретного счёта. Есть счёт пользователя, который динамически меняется в памяти после прихода событиz. Чтобы различить событие для этого счёта используется этот id)
event_type (статус: списание/зачисление)
payload (100 рублей)
created_at (дата)
🔗 Связи с другими паттернами
Часто используется вместе с:
CQRS — команды пишут события, запросы читают витрины
Outbox — публикация событий во внешние сервисы
Saga — управление процессами через события
Materialized View — быстрые чтения
Почитать подробнее про Event Sourcing
Materialized View / Read Mode (Витрина данных)
Допустим у вас модель даннах EventSourcing и у вам много запросов на чтение. Если на лету забирать данные и считать каждый раз - будет очень большая на ваш сервис. Вы создаёте упрощенную отдельную таблицу, например, хеш в Redis (аналог таблицы PostgreSQL), в которую реплицируются данные из основной событийной таблицы на запись, при этом там считается текущее значение (например, складыаются все операции-события по счёту и получается конкретное число на балансе). Это элемент CQRC.
Схема наполнения витрины данных.
Command(запрос на обновление: добавление операции по счёту) → Write DB (+ outbox/event store - записали и отравили в брокер для доставки в read DB) → Broker → Projector(Обсчитали нужные данные) → Read DB (данные записаны в Redis) → Query (Пользователь запросил данные)
Частые заблуждения
❌ «Read Model = копия основной таблицы»
✔ Нет.
Read Model — это проекция, а не реплика. - строки фильтруются, данные пересчитываются
❌ «Read Model обновляется триггером БД»
✔ Иногда — да, но чаще:
через события
через очередь
через CDC
Триггеры — крайний вариант.
❌ «Read Model = BI / DWH»
✔ Нет.
Read Model — это оперативные данные для приложения,
а не аналитика.
🔗 Связи Materialized View / Read Model
CQRS — Read Model реализует сторону чтения
Event Sourcing — Read Model строится из событий
Outbox — гарантирует доставку событий в Read Model
Database per Service — витрина принадлежит одному сервису
Saga — Read Model отражает состояние процесса
Cache — Read Model можно кэшировать (но это не кэш)
Shared Database — антипаттерн по отношению к Read Model
Более подробно про цели витрин читайте здесь
Database Per Service (База данных на сервис)
Описание
Database Per Service — это архитектурный паттерн, при котором каждый микросервис владеет своей базой данных и только он имеет право читать и писать в неё напрямую.
Пример кейса: У 2ух команд общая схема данных, одной команде
Команда A:
«Нам нужно добавить поле payment_channel»
Команда B:
«Это поле нам не нужно, но теперь ORM падает»
Частые заблуждения
❌ «Это одна БД на сервис физически»
✔ Чаще логическое разделение (schema / instance)❌ «Другие сервисы могут читать read-only»
✔ Даже read-only ломает границы❌ «Без Shared DB невозможно делать отчёты»
✔ Для этого есть Read Model / DWH
🔗 Связи с другими паттернами
DDD / Bounded Context — сервис = владелец модели
CQRS / Read Model — чтение без доступа к чужим БД
Saga — согласование данных без общей БД
Shared Database — антипаттерн
Circuit Breaker (Автоматический выключатель)
Описание
Circuit Breaker — это паттерн отказоустойчивости, который временно прекращает запросы к зависимому сервису, если тот работает нестабильно или недоступен.
Он защищает систему от:
бесконечных таймаутов
лавинообразных отказов
исчерпания потоков / соединений
Ключевая идея: если зависимость «умирает» — не добивай её и не умирай вместе с ней.
Какую проблему решает
Что происходит БЕЗ Circuit Breaker
Service A → Service B (упал)
запросы висят по таймауту
потоки Service A забиваются
CPU растёт
Service A тоже падает
Результат — каскадный отказ
Что происходит С Circuit Breaker
несколько ошибок подряд
Circuit Breaker размыкается
новые запросы не отправляются
Service A быстро получает ошибку / fallback
Результат — система деградирует, но не падает
Частые заблуждения
❌ «Circuit Breaker = Retry»
✔ Нет.
Retry повторяет запрос.
Circuit Breaker запрещает его.
❌ «Circuit Breaker нужен только если сервис упал»
✔ Нет. Он нужен:
при таймаутах
при деградации
при частичных сбоях
❌ «Circuit Breaker — это просто if(timeout)»
✔ Нет.
Он работает по статистике ошибок, а не по одному случаю.
Rate Limiting (Ограничение частоты запросов)
Описание
Rate Limiting — это паттерн, который ограничивает количество запросов от клиента или сервиса за единицу времени. Rate Limiting защищает сервис от клиентов, Circuit Breaker — от зависимостей.
Какую проблему решает
Без Rate Limiting
один клиент может «забить» весь сервис
всплеск трафика валит БД
бот / баг / retry-шторм убивает прод
С Rate Limiting
нагрузка контролируемая
сервис остаётся жив
деградация предсказуема
Частые заблуждения
❌ «Rate Limiting = Circuit Breaker»
✔ Нет. Rate Limiting — про входящий поток, CB — про зависимость.
❌ «Rate Limiting решает DoS»
✔ Частично. Для DDoS нужны отдельные инструменты.❌ «Можно не возвращать ошибку»
✔ Нужно явно сигнализировать (HTTP 429).
🔗 Связи с другими паттернами
API Gateway / BFF — основное место применения
Circuit Breaker — защита от downstream
Retry with Backoff — предотвращение штормов
Bulkhead — изоляция ресурсов
Timeout — контроль времени обработки
API Composition / API Gateway (API-шлюз)
Описание
API Composition (API Gateway) — это паттерн, при котором единая точка входа принимает запросы клиентов и:
маршрутизирует их в нужные сервисы,
агрегирует ответы,
применяет кросс-срезную логику - логика, применяемая к каждому запросу (security, лимиты, трейсинг).
Ключевая идея: клиент не знает о микросервисах — он знает только шлюз.
Какую проблему решает
Без шлюза
клиент дергает 5–10 сервисов
сложная клиентская логика
лишний сетевой трафик
дублирование auth / логирования
Со шлюзом
один запрос от клиента
централизованная маршрутизация
единая политика безопасности
меньше чата по сети
Частые заблуждения
❌ «Gateway = бизнес-логика»
✔ Нет, только оркестрация и агрегация.❌ «Gateway = BFF»
✔ Gateway — общий, BFF — под конкретный UI.❌ «Gateway — это SPOF»
✔ Нет, если он stateless и масштабируемый.
🔗 Связи с другими паттернами
BFF — частный случай шлюза
Rate Limiting — защита входа
Circuit Breaker — защита от зависимостей
Retry / Timeout — устойчивость
ACL — изоляция внешних контрактов
Репликация данных (Data Replication)
Описание
Репликация данных — это паттерн, при котором данные одного сервиса копируются (реплицируются) в другой сервис или хранилище для чтения или локального использования.
Источник данных остаётся один, остальные копии — вторичные.
Ключевая идея:
данные можно копировать, но владелец должен быть один.
Какую проблему решает
К примеру, у вас есть мастер система, хранящая данные о клиенте (Единый Клиент - ЕК) , как правила данные о клиенте нужны всем другим систем организации. Вместо того, чтобы каждая система ходила и загружала сеть, можно сделать поток репликации от ЕК до других систем.
Короткая формула
Репликация данных — это способ приблизить данные к потребителю,
не отдавая ему право владения.
Частые заблуждения
❌ «Репликация = Shared Database»
✔ Нет. Репликация — копия, Shared DB — общее владение.❌ «Реплика всегда синхронная»
✔ Обычно eventual consistency.❌ «Реплика может правиться»
✔ Нет, реплика read-only.
Связи с другими паттернами
Read Model / Materialized View — частный случай репликации
CQRS — чтение из реплик
Event-Driven Architecture — доставка изменений
Shared Database — антипаттерн
BFF (Backend for Frontend)
Описание
BFF (Backend for Frontend) — это паттерн, при котором для каждого типа клиента создаётся отдельный backend-сервис, адаптированный под его потребности.
BFF:
похож на API Gateway, но ориентирован не на систему, а на конкретный UI.
Ключевая идея: каждый клиент (мобилка, web, партнёрское приложение) получает API (шлюз), идеально подстроенное под него.
Какую проблему решает
Без BFF
один «универсальный» API
много лишних полей
разная логика на клиентах
сложные контракты
С BFF
простые и узкие контракты
минимальный payload
логика адаптации на сервере
клиенты максимально «тонкие»
Когда применять
есть разные клиенты (web / mobile / partners)
разные требования к payload
важна производительность
мобильные сети / latency
быстрые изменения UI
❗ BFF не содержит бизнес-правил.
Примеры:
/mobile/v1/...
/web/v1/...
/partner/v1/...
Частые заблуждения
❌ «BFF = API Gateway»
✔ Gateway — общий, BFF — клиентский.❌ «BFF = ещё один слой сложности»
✔ Нет, он упрощает клиентов.❌ «Можно сделать один BFF для всех»
✔ Тогда это снова Gateway.
🔗 Связи с другими паттернами
API Gateway — часто стоит перед BFF
Rate Limiting — на входе
ACL — изоляция внешних контрактов
CQRS (Command Query Responsibility Segregation)
Разделение команд и запросов
Описание
CQRS — это архитектурный паттерн, при котором операции записи (Commands) и операции чтения (Queries) разделяются:
логически,
а часто и физически (разные сервисы, БД, модели).
Ключевая идея:
запись и чтение — это разные нагрузки, с разными требованиями.
Какую проблему решает
Без CQRS
одна модель на всё
чтения и записи конкурируют за ресурсы
сложные JOIN’ы мешают транзакциям
масштабирование — только целиком
С CQRS
write-модель оптимизирована под инварианты и транзакции
read-модель оптимизирована под быстрые запросы
масштабирование независимое
Балансировка нагрузки становится управляемой.
Когда применять
чтений сильно больше, чем записей
сложная доменная логика
высокая нагрузка на read
разные SLA для read / write
Event Sourcing или EDA
Типичные кейсы:
балансы
заказы
личные кабинеты
дашборды
Частые заблуждения
❌ «CQRS = Event Sourcing»
✔ Нет, можно использовать отдельно.❌ «CQRS — это два сервиса обязательно»
✔ Не обязательно, можно логически (чтение и запись разделены по коду, моделям и ответственности, но могут жить в одном сервисе и даже в одной БД
🔗 Связи с другими паттернами
Read Model / Materialized View — реализация read-стороны
Event Sourcing — источник событий для read
Outbox — доставка событий
EDA — асинхронное обновление
Database per Service — изоляция данных
BFF / API Gateway — потребители read-стороны
Saga (Распределённые транзакции)
Описание
Saga — это паттерн для выполнения длинных бизнес‑операций, которые затрагивают несколько микросервисов, без использования распределённых ACID‑транзакций (2PC). Пример, у вас был монолит, где бизнес операция имела ACID‑транзакцию в БД и это было безопасно (всегда можно откатиться в случае ошибки). Далее вы разнесли монолит по микросервисам и применили паттерн: «1 сервис одна БД». Здесь обычный ACID можно сделать кросс‑запросом, но это неэффективно по времени. SAGA — набор правил, как можно решить проблему с изолированностью данных (Пример проблемы: на каком‑то этапе бизнес‑операции происходит ошибка при совершении транзакции между микросервисами, в БД это Rollback, а если несколько микросервисов, каждый со своей БД участвует в процессе? Компенсирующие транзакции.)
Ключевая идея: не откатывать всё атомарно, а уметь «откатить бизнесом».
Примечание: При использовании этого шаблона, упоминаются такие версии реализации как Хореография и Оркестрация — это общение между теми самыми сервисами, которые осуществляются распределённую межсервисную транзакцию.
Какую проблему решает
Без Saga
нет общей транзакции
частичные успехи
данные рассинхронизируются
система попадает в неконсистентное состояние
Пример:
заказ создан
деньги списаны
доставка не оформлена ❌ -> откатываем всё компенсирующими транзакциями (функции в коде, которая вычисляет необходимое изменение в сервисе, чтобы вернуть “Как было”)
С Saga
каждый шаг либо:
успешно завершён
либо компенсирован
система приходит к бизнес-консистентному состоянию
Когда применять
операция затрагивает несколько сервисов
нет общей БД
нельзя использовать 2PC
операция «длинная» (секунды, минуты)
допустима eventual consistency
Типичные кейсы:
оформление заказа
платежи + доставка
кредитные заявки
бронирование ресурсов
Когда НЕ применять
одна БД / один сервис
нужна строгая ACID-консистентность
нет компенсирующих действий
операция очень простая
Частые заблуждения
❌ «Saga = ACID транзакция»
✔ Нет, это eventual consistency.❌ «Saga автоматически откатывает всё»
✔ Нет, только то, что ты явно компенсировал.❌ «Компенсация = rollback»
✔ Нет, это отдельная бизнес-операция.
🔗 Связи с другими паттернами
Database per Service — каждая локальная транзакция
Event-Driven Architecture — шаги через события
Outbox — надёжная публикация шагов
CQRS — write-сторона запускает SagaRead Model — отображение статуса Saga
Circuit Breaker / Retry — устойчивость шагов
