Гайд: как системному аналитику построить доменную модель для Java-микросервисов
Всем привет, меня зовут Сергей Прощаев. 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 — там публикуем анонсы открытых уроков, полезные материалы и разборы для тех, кто хочет расти в разработке, архитектуре, аналитике и управлении.
Автор: sproshchaev

