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

Эта статья предназначена для более быстрого запоминания/повторения паттернов микросервисов. Кое‑где я приводил кейсы, которые могут быть непонятны новичкам. Здесь нет подробных кейсов применения каждого паттерна так как иначе статья получилась бы на другую тему. Для удобства я приложил ссылки — чтобы избежать дублей.

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() {...}

}

Проблемы монолита:

  1. Конфликты команд: Команда маркетинга хочет добавить поле videoReview, а команда логистики против — оно им не нужно. Расползание агрегата - усложнение ответа без пользы другой команды.

  2. Медленная разработка: Любое изменение требует полного регресса (везде ли всё поменяли) и согласования со всеми.

  3. Масштабирование невозможно: При пиковой нагрузке на распродажах «падает» весь монолит, включая складской учёт т.к. передаётся слишком большой объём данных из-за разросшегося агрегата. 

  4. Когнитивная перегрузка: Новый разработчик должен понять все 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; }

}

Командна каталога решила Проблемы монолита:

  1. Хочет добавить новые поля - ни с кем не надо согласовывать т.к. увеличит объём обмена данными только для микросервиса команды каталога

  2. Регресс незначим т.к. микросервисы намного проще, разработка быстрая

  3. Маштабирование микросервиса (поднятие новых под) намного дешевле, чем для всего монолита, можно точечно увеличить ресурсы конкретного отдела

  4. Когнитивной перегрузки нет, см. п. 2. 



Когда применять

  • Большая сложная предметная область (банки, страхование, e-commerce)

  • Требуется долгосрочная поддерживаемость

  • Команды работают независимо, нужны границы контекстов

  • Много связанных сущностей, которые нужно «развязать»


    ⚠️ Когда НЕ применять

  • В маленьких проектах (простая CRUD-система)

  • Если команды маленькие (1–2 человека)

  • Если предметная область банально проста.


 Заблуждения

  1. «DDD — это про микросервисы»
    ✔ Нет, DDD может быть и в монолите. (Проще потом разбивать) 

  2. «Bounded Context = таблицы в БД»
    ✔ Нет, это предметная модель, не физические таблицы.

  3. «Один 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 → мы внутри «Кредитования» дальше делим модель на понятные контексты.

 Частые заблуждения

  1. «Business capability = микросервис»
    ❌ Нет. Capability — крупная область (Loans),
    внутри неё может быть много сервисов: loan-offer-service, loan-contract-service, loan-schedule-service.

  2. «Это то же самое, что DDD / Bounded Context»
    ❌ Нет.

    • Capability — организационно-бизнесовый уровень («чем занимается команда/юнит»).

    • Bounded Context — архитектурный уровень модели внутри этой области.
      Обычно: одна capability → несколько bounded context’ов.

  3. «Разберёмся с 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) 

  1. Оберегает бизнес-инварианты (правила, которые нельзя нарушить). Например, в legacy-системе активный клиент это пользователь, заходивший не меньше месяца назад. В нашей АС это пользователь, заходивший не меньше месяца назад, прошедший верификацию KYC и не в чёрном списке. ACL собирает эту информацию с legacy и других систем, поэтому до конца адаптером его назвать нельзя. 

  2. Преобразует коды ответов:

Легаси сервис возвращает:

  • 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-стороны

Более подробно о CQRS

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-сторона запускает Saga

  • Read Model — отображение статуса Saga

  • Circuit Breaker / Retry — устойчивость шагов

Подробнее о SAGA