Всем привет, меня зовут Сергей Прощаев. Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E-commerce, преподаю на курсах разработки и архитектуры в OTUS.
В этой статье разбираю системный подход: как системному аналитику спроектировать доменную модель, которая прямо ложится в код Java-микросервисов, и избежать ситуации, когда архитектор после передачи требований перекраивает всё вслед за словами «это не ложится на DDD».

Когда аналитик останавливается на уровне «система должна регистрировать пользователя и отправлять email», он фиксирует сценарий, но не границы предметных областей. Регистрация — это не один процесс, а несколько:
Управление учётной записью — создать, заблокировать, удалить.
Верификация контактов — отправить код, подтвердить email.
Уведомления — шаблон письма, политика повторов.
Аудит безопасности — кто, когда и откуда менял данные.
Если всё это попадает в один сервис «User», через полгода образуется монолит с высокой связанностью. Разработчикам придётся разбираться, какие намерения стояли за спецификацией, и перекраивать модель вслепую. Решение — начинать не со списка API, а с моделирования предметной области по принципам Domain-Driven Design (DDD).
Шаг 1. Вытащить домен из требований с помощью Event Storming
В одном проекте требовалось спроектировать сервис обработки заказов для B2B-платформы. Вот исходные условия, в которых мы работали:
Проект: B2B-платформа оптовых заказов.
Команда: 2–3 разработчика, продакт, системный аналитик.
Стек: Java 17, Spring Boot, Kafka.
Ограничение: строгие требования к консистентности финансовых данных — скидки и счета не должны расходиться.
Бизнес-формулировки были стандартными: «пользователь оформляет заказ, система считает скидки, отправляет уведомление менеджеру». Прямой путь вёл к транзакционному скрипту. Вместо этого я провёл двухчасовую сессию Event Storming с продактом и двумя разработчиками — только стикеры и вопросы: «какое событие происходит?», «кто его вызывает?», «нужно ли немедленное действие?».
Мы выстроили события:
Заказ отправлен
Скидка рассчитана
Заказ подтверждён
Счёт-фактура создана
Уведомление отправлено менеджеру
Почти сразу стало видно, что «расчёт скидки» и «создание счёта-фактуры» принадлежат разным контекстам: первое — ценообразованию, второе — биллингу/документообороту. События, которые сначала казались линейными, начали группироваться вокруг двух процессов: «Обработка заказа» и «Выставление счёта». Так родилась первая версия ограниченных контекстов (Bounded Context), показанная на рис. 2.
На этом этапе я ещё не думаю о микросервисах. Я думаю о границах модели. Сервисы появляются позже — как способ развернуть эти границы независимо.

Главный вывод после этой схемы: контексты не соответствуют одному экрану или одному API. «Ценообразование» определяет скидки независимо от бухгалтерского учёта, а «Биллинг» отвечает за финансовые документы. Как только это зафиксировано, мы с архитектором сверили ядро: получилось три отдельных сервиса, а не один «Order Service». Аналитик здесь выступает соавтором границ сервисов, а не просто транслятором требований. Фактически он начинает влиять на архитектуру до того, как написана первая строчка кода.
Проверка: если вы можете чётко сказать, какие события принадлежат какому контексту, и ни одно событие не требует одновременного изменения двух контекстов, — границы найдены верно.
Шаг 2. Перенести модель в Java-код без анемии
Теперь проектирую черновую структуру пакетов и ключевые интерфейсы на Java. Они выражают намерение точнее текста и сразу дают разработчикам опору для реализации.
Пример структуры для контекста «Заказ»:
// Пакет: com.example.order.domain public class Order { private OrderId id; private CustomerId customerId; private List<OrderLine> lines; private OrderStatus status; private Money total; // Подтверждение заказа возможно только после получения результата ценообразования, // даже если скидка равна нулю. Это позволяет агрегату гарантировать, // что финальная стоимость согласована с контекстом Pricing. public void confirm(Discount discount) { if (lines.isEmpty()) { throw new EmptyOrderException(); } if (!discount.isApplicableTo(this)) { throw new InvalidDiscountException(); } this.total = discount.applyTo(calculateGrossTotal()); this.status = OrderStatus.CONFIRMED; registerEvent(new OrderConfirmed(id, total)); } private Money calculateGrossTotal() { /* ... */ } }
// Абстракция доступа к агрегатам в доменном слое public interface OrderRepository { Optional<Order> findById(OrderId id); void save(Order order); }
Агрегат предоставляет метод проверки применимости скидки, не раскрывая деталей вычисления хэша позиций. Это видно в реализации Discount:
// Discount — Value Object, содержащий идентификатор заказа или хэш позиций. // Это гарантирует, что скидка применима именно к текущему состоянию агрегата. public record Discount(OrderId orderId, Money amount, CustomerSegment segment, int positionsHash) { public boolean isApplicableTo(Order order) { // Хэш используется, чтобы не раскрывать состав заказа вне агрегата return order.getId().equals(orderId) && order.positionsHash() == positionsHash; } } // Контекст ценообразования — отдельный доменный сервис public interface DiscountCalculator { Discount calculate(List<OrderLine> lines, CustomerSegment segment); }
Я размещаю такой код прямо в Confluence рядом с User Story. Разработчики видят не «система должна рассчитать скидку», а место в модели, куда эта скидка встанет, и инварианты, которые гарантируют целостность. Споров о структуре становится вдвое меньше — не нужно угадывать замысел аналитика.
Без предварительного моделирования путаница неизбежна. Спецификация на сорок страниц без единого фрагмента кода, зато с десятками UML-диаграмм, которые ничего не говорят о бизнес-инвариантах, часто скрывает тот факт, что «расчёт комиссии» — отдельный контекст, а не метод внутри Order.
Проверка: попробуйте мысленно выполнить сценарий «подтверждение заказа без скидки». Если агрегат не падает и сохраняет инвариант, структура выдерживает базовый тест.
Шаг 3. Согласовать взаимодействие через интеграционные события
После построения модели я провожу 30-минутную синхронизацию с командой. Показываю три вещи: предлагаемые пакеты, расположение бизнес-правил и интеграционные события, пересекающие границы контекстов (рис. 3).

Важно: заказ не «ждёт» скидку синхронно. После публикации OrderPlaced он переходит в статус PRICING_PENDING, а подтверждение происходит отдельной командой после получения события DiscountCalculated. Так каждый контекст остаётся автономным, и распределённой транзакции не требуется.
Схема на рис. 3 сразу показывает, что расчёт скидки — асинхронная операция. Становится очевидной необходимость асинхронной доставки событий. Аналитик не обязан диктовать реализацию, но обязан подсветить места, где синхронный подход привёл бы к жёсткой связи. На практике это экономит недели рефакторинга.
Проверка: убедитесь, что при «падении» сервиса Pricing, заказ остаётся в статусе
PRICING_PENDINGи может быть подтверждён позже, когда ценообразование восстановится.
Лучшая практика: границы контекстов и границы команд
Вспоминается реальный случай из финтех-проекта. Команды «Платежи» и «Клиенты» постоянно блокировали релизы друг друга: любое изменение в карточке клиента ломало интеграции с платёжным шлюзом. Причина — общая модель Customer, используемая обеими командами. Как только мы, системные аналитики, предложили развести её на два ограниченных контекста — Identity(ФИО, паспортные данные) и PaymentProfile (карты, лимиты) — конфликт исчез. Модель стала развязанной, и границы контекстов совпали с границами ответственности команд. Этот шаблон теперь применяю в большинстве проектов.
В стартапе из трёх человек такой подход может быть избыточен, но как только команд становится больше, явные контексты становятся обязательным условием независимой разработки.
Что делать с этим знанием
Системный аналитик, владеющий Event Storming, выделением Bounded Context и проектированием агрегатов, перестаёт быть просто составителем требований. Он влияет на архитектуру микросервисов, а разработчики получают модель, а не гадание. Если вы хотите освоить этот переход системно и попрактиковаться в построении доменных моделей под руководством практиков, обратите внимание на курс OTUS «Системный аналитик: проектирование и DDD». На нём мы разбираем реальные кейсы вплоть до Java-реализации.

Когда требования описаны подробно, но доменная модель не собрана, команда всё равно приходит к хаосу: один сервис разрастается, бизнес-правила размазываются по коду, а любое изменение начинает ломать соседние контексты.
Чтобы такого не происходило, приходите на демо-уроки:
2 июня, 20:00. «Объектная модель без боли: как превратить хаос требований в стройную архитектуру».
Поговорим о том, как из разрозненных требований собрать понятную объектную модель и не потерять бизнес-логику при переходе к реализации.4 июня, 20:00. «C4 для системного аналитика: строим единый язык между бизнесом и разработкой».
Покажем, как описывать системы так, чтобы бизнес, аналитики, архитекторы и разработчики одинаково понимали границы, зависимости и ответственность компонентов.17 июня, 20:00. «Архитектура информационных систем. Монолиты, SOA и микросервисы».
Разберем, чем отличаются архитектурные подходы и как не превратить микросервисную систему в распределённый монолит.
Уроки бесплатные, проходят в рамках онлайн-курсов OTUS и помогают познакомиться с преподавателями-практиками, протестировать формат обучения и задать вопросы.
А ещё подписывайтесь на канал OTUS в MAX — там публикуем анонсы открытых уроков, полезные материалы и разборы для тех, кто хочет расти в разработке, архитектуре, аналитике и управлении.
