Когда есть бизнес-бриф маркетплейса и нужно написать первую строчку кода — возникает соблазн сразу начать с деталей: какой агрегат у заказа, какие таблицы в базе, как устроена Saga. Это ошибка. До всего этого нужно ответить на более простой вопрос: какие вообще сервисы будут в системе и где между ними граница.
Без этого шага вы получите код, в котором Catalog считает комиссию, Order пишет в каталог, а Payment знает про скидки. Через полгода это переписывание с нуля.
В этой статье — восемь шагов: от брифа до карты сервисов с границами, интеграциями и планом отказов.
Шаг 1. Собрать доменные события
Первый шаг называется Event Storming: берём бриф и выписываем всё, что в системе происходит, в порядке времени. Не «сущности» и не «таблицы» — а события: «покупатель добавил товар в корзину», «продавец подтвердил заказ», «модератор отклонил карточку».
Формат простой: каждое событие — глагол в прошедшем времени. «Заказ создан», «платёж принят», «карточка одобрена».
Из брифа маркетплейса получается около 50 событий. Они сами группируются в потоки:
- Каталог: продавец загрузил карточку → модератор одобрил → покупатель добавил в избранное → карточка снята с продажи.
- Покупка: корзина создана → товар зарезервирован → заказ оплачен → заказ доставлен → покупатель подтвердил.
- Деньги: платёж принят → деньги в эскроу → выплата ушла продавцу → возврат запрошен.
- Уведомления: отправить продавцу о новом заказе → отправить покупателю об отгрузке → напомнить о подтверждении.
- Споры: спор открыт → оператор взял в работу → возврат денег.
- Авторизация: покупатель залогинился → продавец прошёл двухфакторную проверку.
Эти потоки — будущие границы сервисов. Не каждый поток становится отдельным сервисом, но каждый сервис вырастает из одного-двух потоков.
Шаг 2. Найти границы контекстов
Из событий выделяем Bounded Contexts — части системы, у каждой из которых свой язык. Звучит абстрактно, вот конкретный пример: слово «заказ» в платёжной части означает «финансовый документ с зарезервированными деньгами», а в каталоге — «строка для аналитики». Если этим двум смыслам дать одно имя в одной базе, через год никто не поймёт, что куда.
Для маркетплейса напрашиваются шесть контекстов:
| Контекст | Что внутри |
|---|---|
| Catalog | Товары, карточки, поиск, модерация |
| Order | Корзина, заказ, резерв товара, статусы |
| Payment | Платежи, эскроу, выплаты, комиссия |
| Notification | Каналы (email/sms/push), шаблоны, отправка |
| Customer | Покупатели, продавцы, регистрация, авторизация |
| Backoffice | Модерация, споры, аудит действий оператора |
Это кандидаты на сервисы. Вопрос «делать ли микросервисы» этот шаг ещё не решает — он только говорит, где естественные линии разлома.
Шаг 3. Решить: монолит или микросервисы
Это отдельное решение, которое зависит от нагрузки, сроков и состава команды. Для маркетплейса исходные данные такие:
- Нагрузка: десятки тысяч запросов в секунду на чтение каталога, тысячи на оформление заказов, единицы на платежи. Чтение каталога надо масштабировать независимо.
- Сроки: первая версия за шесть месяцев, четыре команды разработки.
- Соответствие требованиям: Payment обязан быть изолирован по требованиям PCI-DSS.
Решение — микросервисы по контекстам, без дробления внутри контекста:
- Catalog — отдельно, потому что нагрузка на чтение и команда поиска отдельные.
- Order — отдельно, потому что нельзя терять резервы и платежи.
- Payment — отдельно по требованиям безопасности (PCI-DSS scope изолируется).
- Notification — отдельно по нагрузке и интеграциям с внешними провайдерами.
- Customer и Backoffice — каждый свой сервис, без серьёзной нагрузки.
Что не делаем: не дробим Catalog на Search, Pricing и Reviews ради «правильной архитектуры». Линии разлома идут по событиям и контекстам, не по техническим слоям.
Шаг 4. Нарисовать карту с границами
Получаем шесть сервисов. Для каждого важно зафиксировать не только что он делает, но и что не делает — это важнее.
Ограничения каждого сервиса:
- Catalog не считает деньги. Цена в карточке — справочная, реальная цена фиксируется в Order при оформлении.
- Order не пишет в каталог. Резерв товара — это запись в Order, не в Catalog.
- Payment не знает про заказы. Только про платежи и планы выплат. Связь с заказом — через внешний идентификатор.
- Notification ничего не решает. Только отправляет по команде. Логика «когда уведомить» — в источнике события.
- Customer не хранит товарный профиль продавца. Customer — это identity и авторизация, отдельно от каталога.
- Backoffice не правит данные напрямую — действия идут через API соответствующего сервиса с записью в аудит.
Эти ограничения — самое ценное в карте. Без них через год Catalog начнёт считать комиссию, а Order — писать в каталог.
Шаг 5. Определить, кто чем владеет
Каждая сущность имеет ровно одного владельца. Все остальные читают через интеграции, не лезут в чужую базу напрямую.
| Сущность | Владелец | Кто читает |
|---|---|---|
| Карточка товара | Catalog | Order (снимок при оформлении) |
| Остаток товара | Catalog | Order (через резерв) |
| Заказ | Order | Payment, Notification, Backoffice |
| Платёж / эскроу | Payment | Order (статус), Backoffice |
| Выплата продавцу | Payment | Backoffice |
| Профиль покупателя | Customer | Order, Notification |
| Профиль продавца | Customer | Catalog, Payment, Backoffice |
| Авторизационный токен | Customer | Все (через JWT) |
| Шаблон уведомления | Notification | — |
| Аудит действий | Backoffice | — |
Несколько правил, которые отсюда следуют:
Снимок при пересечении границы. Когда Order оформляется, цена и название товара копируются в заказ. Если продавец завтра изменит карточку — уже созданный заказ не меняется. Это правильно: покупатель должен получить то, на что согласился.
Чужой идентификатор, не структура. Order ссылается на товар по productId, не тащит к себе всю карточку. Чужую структуру он не дублирует.
Для отчётов — отдельная модель чтения. Backoffice читает через API или через поток событий, не делает JOIN на чужие базы данных.
Шаг 6. Выбрать, как сервисы общаются
Четыре варианта взаимодействия, каждый для своего случая:
Синхронные вызовы (REST) — когда нужен ответ прямо сейчас:
- Web → Catalog (показать карточку товара)
- Web → Order (создать заказ)
- Order → Catalog (зарезервировать остаток)
- Web → Customer (войти в систему)
Асинхронные события через Kafka — когда сервис должен отреагировать, но блокировать вызывающего не нужно:
OrderCreated→ Notification (отправить уведомление продавцу), Payment (создать платёжное намерение)PaymentCaptured→ Order (перевести в статус «оплачен»)CardModerated→ Catalog (опубликовать карточку), Notification (сообщить продавцу)
Saga для распределённой операции «оформление заказа» — резерв товара (Catalog) + платёж (Payment) + создание заказа (Order). Если любой шаг упал — остальные откатываются. Подробнее — в спецификации Order Service.
Transactional Outbox — чтобы событие не потерялось. Событие сохраняется в отдельную таблицу в одной транзакции с бизнес-данными. Отдельный процесс периодически читает её и публикует в Kafka. Это гарантирует, что событие не исчезнет, если сервис упал между записью в базу и отправкой в очередь.
Что не делаем:
- Нет двусторонней синхронизации. Если Catalog изменился — Order узнаёт через снимок или событие, не через «переспроси и обнови у себя».
- Нет общей базы данных. Даже в начале — сервисы не разделяют схему PostgreSQL.
- Нет цепочек из пяти синхронных вызовов. Один пользовательский запрос — не больше двух синхронных переходов между сервисами.
Шаг 7. Продумать поведение при отказах
Для каждого синхронного взаимодействия нужен план: что происходит, если сосед недоступен.
| Падает | Кто страдает | Что делаем |
|---|---|---|
| Catalog | Web, Order | Web показывает закешированные карточки. Order отказывает в оформлении — нельзя резервировать без актуального остатка. |
| Order | Web | Web показывает «оформление недоступно». Платёж не создаётся. |
| Payment | Order | Saga ставит платёж в режим ожидания, повторные попытки до 24 часов. После — отмена заказа. |
| Customer | Все | JWT проверяется локально по публичному ключу, без обращения к Customer. Только новые входы в систему недоступны. |
| Notification | — | Никто не страдает. События остаются в Kafka, уведомления придут позже. |
| Backoffice | — | Аудит накапливается в очереди, оператор разбирает позже. |
Главный принцип: падение Notification и Backoffice не влияет на покупки. Падение Catalog или Order влияет, но с плавной деградацией — старый кеш, отказ только в новых заказах, а не во всём сайте.
Эти решения принимаются до детального проектирования каждого сервиса. Если спроектировать сервис, не зная его поведение при отказе соседа, придётся переделывать обработку ошибок и механизм повторных попыток.
Шаг 8. Визуализировать итог
Финальный артефакт — диаграмма уровня контейнеров (C4 Container). Это то, что показывают новому разработчику и на техническом разборе с заказчиком.
Каждый узел на схеме — отдельный процесс, своя база данных, свой деплой. Что внутри каждого — описывается в его собственной спецификации.
Дальше
После карты сервисов каждый сервис получает детальную спецификацию на нужном уровне сложности:
- Catalog — CQRS: чтение через Elasticsearch, запись через PostgreSQL.
- Order — готовая спецификация: три агрегата, Saga, Outbox, управление доступом.
- Payment — строгая идемпотентность, изоляция по PCI-DSS.
- Notification — готовая спецификация: отправка без привязки к каналу.
- Customer — OAuth2 + JWT, без сложной доменной логики.
- Backoffice — CRUD плюс аудит.
Уровень детальности выбирается по сложности домена, а не ради единообразия. Catalog читают миллионы пользователей — нужен CQRS. Backoffice обрабатывает десять запросов в минуту — агрегаты там не нужны.
Коротко
- Начинать с событий, не с сущностей: Event Storming выявляет естественные группы — будущие контексты.
- Bounded Context — часть системы со своим языком: одно слово в разных контекстах значит разное.
- Решение «монолит или микросервисы» принимается отдельно, на основе нагрузки, команды и требований.
- Граница сервиса — это то, что он не делает, не только то, что делает.
- У каждой сущности ровно один владелец; остальные читают через API или события.
- Синхронные вызовы — когда нужен ответ сейчас; асинхронные события — когда блокировать не нужно.
- Saga нужна для операций, которые затрагивают несколько сервисов и должны откатываться при сбое.
- Transactional Outbox гарантирует, что событие не потеряется при падении сервиса.
- Поведение при отказе соседа продумывается до детального проектирования, не после.
Что почитать дальше
- Спецификация Order Service — как выглядит детальная спецификация сервиса на примере Order.
- Спецификация Notification Service — простой сервис с Уровнем 1 сложности.
- Бизнес-бриф маркетплейса — исходная точка, откуда начинался этот разбор.