Всем привет, меня зовут Сергей Прощаев. Последние несколько лет я проектирую высоконагруженные системы в финтехе и постоянно возвращаюсь к одному и тому же вопросу.

Мы в IT умеем красиво рисовать кубики на архитектурных радарах. Сервис заказов, сервис доставки, сервис уведомлений. Кубики соединяются стрелочками — обычно RESTом или Kafka. Всё выглядит стройно.

Проблема в том, что эта стройность чаще всего иллюзорна

Встречал видел проекты, где «микросервисная архитектура» означала просто нарезанный монолит, обмотанный синхронными HTTP-вызовами. И в такой схеме если падал сервис пользователей — то за ним следом по цепочке валились ещё пять. Согласованность данных держалась на честном слове и ad-hoc скриптах. А на вопрос «почему граница сервисов проходит именно здесь?» разработчики пожимали плечами: «так исторически сложилось».

В этой статье я не буду в сотый раз перечислять паттерны из книги Криса Ричардсона, а покажу ход мыслей. Эти мысли будут на предмет того, как на старте не наступить на грабли и почему Domain-Driven Design — это не про «Entity» и Value Object», а в первую очередь про власть и границы, и в какой момент паттерн «Сага» становится вашим единственным спасением (ну или проклятием).

Давайте начнем.


Ловушка №1. «Просто разделим по слоям»

Представь: есть монолит — классическая трёхзвенка. Контроллеры, сервисы, репозитории, общая БД.

Команда решает: «Пора на микросервисы!» И первая мысль: давайте отделим слой работы с данными. Сделаем сервис «Database API», который будет отдавать данные по ID. И второй сервис — «Business Logic», который будет дёргать первый.

Поздравляю, вы только что изобрели распределённый монолит с задержкой в 30 миллисекунд вместо 2.

Уже через месяц или два в такой архитектуре непременно появятся:

  • циклические зависимости в рантайме, когда сервис А вызывал Б, Б вызывал В, В снова лез в А;

  • невозможность выкатить обновление без синхронного релиза трёх (или более) репозиториев;

  • падение всей системы при отказе одного инстанса БД.

Какой из этого можно сделать вывод?

Все очевидно — паттерн декомпозиции по техническим слоям (UI → Logic → Data) в микросервисной архитектуре — почти всегда приводит к плачевному результату, ведь он нарушает главный принцип: сервисы должны быть независимы по бизнесу, а не по функциям.

Давайте рассмотрим основные стратегии, с которых нужно было начать этот процесс.

Стратегия 1. Резать по бизнес-возможностям (и немного DDD)

В первой стратегии необходимо придерживаться золотого правила архитектуры, написанного кровью многих архитекторов, потерпевших неудачи: границы сервиса должны совпадать с границами поддомена!

Здесь нам нужен DDD. Но я не про тактический DDD (Entity, Aggregate Root). Я про стратегический DDD — про Bounded Context.

Как это выглядит в реальности?

Допустим, у нас есть интернет-магазин или маркетплейс, который предлагает посетителям разное барахло. Ассортимент товаров нас интересует меньше всего, а вот на архитектуре мы остановимся. Пусть это будет монолит по схеме «все в одном флаконе»: заказы, склад, оплата, клиенты.

Как можно накосячить сделать все не так как надо? Это просто — сделать сервис «Работа с базой данных заказов»

А как поступит Крис Ричардсон? Не знаком лично с Крисом, но уверен, что он сначала сделает сервис «Обработка заказа» (Order Fulfillment) и затем напилит создаст отдельный сервис «Управление каталогом» (Catalog).

Теперь спросите: «Chris, why is that?» Потому что у этих контекстов разная скорость изменения и разные эксперты. Каталог меняет маркетолог каждую неделю. Процесс заказа трогать без тестирования неделями страшно рискованно.

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

Если «цена товара» нужна и в каталоге, и в заказе — то это еще не повод объединять сервисы. Это повод задуматься, чья это ответственность. В каталоге цена — атрибут, а в заказе — снэпшот на момент покупки.

Граница сервиса — это граница автономии

Давайте возьмем в руки Mermaid и представим — как может выглядеть карта bounded context для нашего гипотетического ритейла интернет-магазина. То, что получилось изображено на рисунке 1:

Рис 1. Карта bounded context интернет-магазина
Рис 1. Карта bounded context интернет-магазина

Стратегия 2. Strangler Pattern — когда монолит ещё жив

Мы и все, что вокруг нас находится не в вакууме. И если вы читаете эту статью, то наверняка у вас есть legacy-система, которую нельзя остановить и которая приносит прибыль бизнесу. И здесь наилучшим образом Крис Ричардсон предложил бы вам рассмотреть применение паттерна «Душитель» (Strangler Fig) 

Если лень читать по ссылке, то вкратце раскрою суть маньяка душителя — новую функциональность пишем сразу в микросервисах, а старую оставляем жить legacy и постепенно переводим на сервисы.

Такая схема оптимально ложиться на схему в которой используется фасад: старый монолит и новый сервис стояли рядом. Фасад смотрел: если запрос по новому API — шлём в новый сервис, если старый — в монолит.

Рис. 2 Вариант использования паттерна «Душитель»
Рис. 2 Вариант использования паттерна «Душитель»

Через год-полтора весь трафик уйдет в новую систему, и монолит можно по-тихому отключить и выпить шампанского.

Критическое условие. Фасад не должен содержать бизнес-логику! Фасад содержит только маршрутизацию, иначе он сам станет монолитом.

В архитектурных практиках есть несколько вариантов реализации паттерна «Душитель» в некоторых запрос от Клиента с Фасада может уходить сначала в Legacy Монолит и далее уже Монолит его адресует в новый выделенный сервис.

Стратегия 3. Database per Service

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

Паттерн Database per Service — обязателен но он порождает главную проблему микросервисов: как делать JOIN через границы сервисов?

Вот здесь мы подходим к самому интересному.

Как работать с данными: CQRS и Saga

1. CQRS — не ради моды, а ради изоляции

Если чтение данных сложное, требует агрегации из 5 сервисов, а запись — простая вставка в одну таблицу — не лучшим вариантом будет использования REST-агрегатора.

Лучше разделить Command и Query и держать отдельную read-модель, которая обновляется асинхронно по событиям.

В одном проекте админка требовала «показать все заказы клиента с его бонусами и историей доставок». В монолите — один SELECT с 3 JOIN. В микросервисах — пришлось дёргать 4 сервиса синхронно. Время ответа — 800 мс.

Сделали простую материализованную вьюху в отдельной read-БД, которая обновлялась по CDC из исходных сервисов. Время упало до 40 мс. Да, это Eventual Consistency. Но для аналитики админа это ок!

2. Saga — когда ACID уже не спасает

Распределённые транзакции — наша плата за независимость.

Здесь есть два подхода.

Хореография. Сервисы слушают события и сами решают, что делать. Минус — логика размазана по коду, через полгода никто не помнит, кто на какие события подписан.

Оркестрация. Один класс (SagaOrchestrator) говорит: «шаг 1 — сервис А, шаг 2 — сервис Б, если ошибка — запусти компенсацию».

Я предпочитаю оркестрацию. Да, это «единый контролёр», но зато поток транзакции читается как сценарий теста.

Три вопроса, которые я задаю себе перед тем, как создать новый сервис

Иногда бывает и больше трех, но три это показатель мастерства. Начинаем последовательно:

  1. Может ли этот сервис жить без другого?
    Если для ответа на запрос мне нужно синхронно сходить в соседний сервис — у меня плохо с автономией. Возможно, граница проведена не там.

  2. Что произойдёт, если база данных этого сервиса упадёт?
    Если упадёт всё приложение целиком — это плохой сервис. Хороший сервис кэширует, отдаёт fallback или хотя бы вежливо говорит «недоступно».

  3. Я смогу переписать этот сервис на другом языке через год?
    Если да — автономия есть. Если нет, потому что он слишком завязан на shared-коде — это распределённый монолит.

Заключение: главный паттерн — это осознанность

Микросервисы — это не про технологию. Это про границы.

Паттерны декомпозиции (по бизнес-возможностям, по поддоменам, Strangler) нужны не для красоты. Они нужны, чтобы сделать границы осмысленными. Когда каждый сервис — это чей-то «кусочек власти» и ответственности, код начинает структурировать сам себя.

Если вам кажется, что я рассказываю очевидные вещи — это хорошо. Значит, вы уже прошли через катастрофы распределённых транзакций и настройку circuit breaker.

А если вы только начинаете и хотите не просто «знать паттерны», а понимать, как их выбирать под конкретную нагрузку и бюджет, — приглашаю на открытый урок курса «Highload Architect» в OTUS 18 февраля в 20:00. Участие бесплатное, нужна регистрация.

Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.

Полный список бесплатных уроков от преподавателей курсов можно посмотреть в календаре мероприятий.