Всем привет, меня зовут Сергей Прощаев, и в этой статье я расскажу про одну из самых сложных, но в то же время интересных тем в проектировании микросервисов — бизнес‑логику. За годы работы над разными проектами — от классического монолита до высоконагруженных распределенных систем — я пришел к выводу, что именно бизнес‑логика является тем самым сердцем приложения. Но в микросервисной архитектуре это сердце начинает биться в ритме, который требует совершенно иного подхода к проектированию.
Мы часто думаем, что микросервисы — это про технологии, контейнеризацию и оркестрацию. На самом деле, микросервисы — это в первую очередь про управление сложностью. И если вы неправильно организуете бизнес‑логику внутри каждого сервиса, ваша распределенная система превратится в монолит с распределенными страданиями. Сегодня мы разберем, как избежать этого, используя концепции из мира предметно‑ориентированного проектирования (DDD), которые я считаю обязательными для любого архитектора.
Вместо традиционного примера с заказами и ресторанами, как в классических книгах, я предлагаю рассмотреть другой, не менее популярный домен — управление корпоративными сотрудниками (HRM). Допустим, у нас есть платформа, которая управляет жизненным циклом сотрудника: от найма до увольнения. Наша задача — спроектировать бизнес‑логику так, чтобы она была устойчивой, тестируемой и не приводила к коллапсу при масштабировании.
Две стороны одной медали: сценарий транзакции против доменной модели
Когда вы только начинаете писать сервис, особенно под давлением дедлайнов, руки сами тянутся к процедурному коду. Вы создаете класс EmployeeService, добавляете метод hireEmployee() и начинаете туда писать: проверить email, сохранить в базу, отправить уведомление. Это и есть Сценарий транзакции (Transaction Script). Мартин Фаулер в своей классической книге описывает его как процедурную организацию бизнес‑логики, где один метод обрабатывает один запрос из UI.
В одном из моих проектов на стартап‑стадии это казалось идеальным решением. За три месяца мы написали 40 таких сценариев. Но когда бизнес‑логика усложнилась (появились правила о том, что сотрудник не может быть руководителем сам себе, ограничения по пересечению грейдов, сложная логика расчета KPI), этот подход дал трещину. Классы раздувались до 1000+ строк, а логика дублировалась. Один метод updateEmployee использовался и для смены отдела, и для повышения, и для переименования, обрастая флагами и if-else. Поддерживать это стало кошмаром.
Альтернатива — Доменная модель. Это не просто набор классов с геттерами и сеттерами. Это модель, где объекты обладают не только состоянием, но и поведением. Вместо того чтобы вызывать внешний сервис для проверки правил, вы говорите объекту: employee.promoteTo(Manager.class), и он сам решает, можно ли это сделать, и какие правила при этом нарушаются.
Я долгое время сопротивлялся внедрению DDD в проект, где «все горит», считая это оверинжинирингом. Но опыт показал обратное: если бизнес‑логика сложнее CRUD, использование Transaction Script неизбежно ведет к техническому долгу. Сложность никуда не девается — она просто переходит из кода в головы разработчиков, которые пытаются запомнить все побочные эффекты.
Агрегат как спасение от хаоса
Но просто сделать объектную модель недостаточно. В микросервисной архитектуре есть две ключевые проблемы, которые убивают традиционную объектную модель:
Ссылки через границы. Если у вас есть объект
Employee, который напрямую ссылается на объектDepartmentиз другого сервиса, вы теряете независимость.Транзакции. ACID‑транзакции не работают между сервисами.
Здесь на сцену выходит Агрегат (Aggregate) — концепция из DDD, которую я считаю лучшим строительным блоком для микросервисов. Агрегат — это кластер объектов, с которыми мы работаем как с единым целым. У него есть корневая сущность (Root), и именно через нее происходит все взаимодействие.
Давайте посмотрим на наш HR‑сервис. Вместо того чтобы создавать паутину из взаимосвязанных классов Employee, Contract, Address, ContactInfo, мы определим агрегат EmployeeProfile. Вот как это выглядит визуально:

Обратите внимание на ключевую деталь: departmentId — это не объектная ссылка на Department, а просто идентификатор. В книге Криса Ричардсона это правило названо одним из ключевых. Мы избавляемся от объектных ссылок, которые пересекают границы сервисов, заменяя их на ссылки по первичному ключу.
Почему это важно? В сети есть описание инцидента в одном из финтех‑проектов, где разработчики использовали Hibernate с ленивой загрузкой между агрегатами. В итоге во время одного из релизов получили миллион N+1 запросов к базе, потому что при загрузке одного объекта цеплялась вся графовая модель, которая уже не помещалась в одну транзакцию. Переход на ссылки через ID снизил связанность и позволил нам четко разделить транзакционные границы.
Правила агрегации, которые требуют внимания
DDD требует, чтобы агрегаты подчинялись набору правил. Эти правила — не догма, а прагматичный инструмент для снижения сложности в распределенных системах.
Правило 1. Транзакция = один агрегат
Самое контринтуитивное правило. В монолите мы привыкли оборачивать в транзакцию обновление десятка таблиц. В микросервисе транзакция базы данных может обновлять только один экземпляр агрегата.
Когда я впервые прочитал это в книге Эрика Эванса, я подумал: «Это же неудобно». И действительно, неудобно. Но это ограничение заставляет нас мыслить в терминах согласованности в конечном счете. Если нам нужно завести сотрудника и создать для него аккаунт в соседнем Auth‑сервисе, мы не можем делать это в одной транзакции.
Решение — Повествование (Saga). Мы либо используем хореографию (события), либо оркестрацию (центральный координатор). В нашем HR‑сервисе мы пошли по пути оркестрации, создав Saga HireEmployeeSaga. Первый шаг — создаем агрегат EmployeeProfile со статусом PENDING. Второй шаг (уже в другом сервисе) — создаем учетную запись. Если что‑то пошло не так, мы откатываем изменения компенсирующими действиями.
Правило 2. Внешние ссылки — только через ID
Это правило мы уже затронули. Но я хочу подчеркнуть его важность на примере нефункциональных требований. Когда агрегаты ссылаются друг на друга по ID, их становится невероятно просто шардировать (сегментировать). Вам не нужно заботиться о том, чтобы связанные объекты лежали в одной ноде. Это делает масштабирование базы данных (например, в MongoDB или Cassandra) тривиальной задачей.
Правило 3. Размер имеет значение
Как понять, где заканчивается один агрегат и начинается другой? Это, пожалуй, самое сложное решение для архитектора. Я использую простое эмпирическое правило: агрегаты должны быть мелкими, насколько это возможно, но настолько крупными, насколько это необходимо для обеспечения инвариантов.
В нашем примере я мог бы сделать EmployeeProfile гигантским и включить в него всех подчиненных сотрудника. Тогда увольнение руководителя атомарно блокировало бы изменения всех его подчиненных. Но это убило бы масштабируемость. Обновления по разным руководителям сериализовались бы в одном агрегате, создавая узкое место.
Мы выбрали компромисс: EmployeeProfile содержит только самого сотрудника и его контракт. Управление иерархией вынесено в отдельный агрегат TeamHierarchy. Если менеджер увольняется, Saga сначала проверяет, что у него нет активных подчиненных, и только потом обновляет агрегат EmployeeProfile.
Доменные события: мост между сервисами
Агрегаты — это хорошо, но как они взаимодействуют в распределенной системе? Синхронные вызовы REST между сервисами для обновления данных — это путь к созданию распределенного монолита. Мы используем Доменные события.
В моей практике был случай, когда команда долго спорила, как синхронизировать данные между HR‑сервисом и сервисом зарплат (Payroll). Синхронный вызов приводил к тому, что при сбое Payroll не мог уволиться ни один сотрудник. Асинхронные события решили проблему.
Доменное событие — это, по сути, глагол в прошедшем времени, который описывает факт изменения состояния агрегата. Например, EmployeeHiredEvent, DepartmentChangedEvent, ContractTerminatedEvent.
Мы реализуем генерацию событий прямо в методах агрегата. Это гарантирует, что событие не будет потеряно. Вот как это обычно выглядит в коде на Java (листинг концептуальный, но отражает суть):
public class EmployeeProfile { public ResultWithEvents hire(HireCommand cmd) { // Бизнес-логика: проверка уникальности, валидация this.status = PENDING; // Возвращаем событие return new ResultWithEvents(this, new EmployeeHiredEvent(cmd.getEmail())); } }
Сервисный слой затем берет это событие и публикует его в брокер сообщений (Kafka, RabbitMQ) в рамках той же транзакции, что и сохранение агрегата. Это ключевой момент. Без транзакционной публикации вы рискуете получить состояние, когда агрегат сохранен, а событие не ушло, или наоборот.
Простая самописная реализация с таблицей OUTBOX (Transactional Outbox) решают эту проблему. Мы используем подход с таблицей OUTBOX: при сохранении агрегата мы пишем событие в ту же базу данных в таблицу outbox. Отдельный процесс‑публикатор читает эту таблицу и отправляет сообщения в брокер. Это гарантирует надежность.
Реальный кейс: история про увольнение 5000 сотрудников
В сети можно найти историю, которая отлично иллюстрирует важность продуманного проектирования бизнес‑логики. В одном крупном ритейлере стояла задача мигрировать HR‑систему с устаревшего монолита на микросервисы. Сервис должен был управлять профилями сотрудников.
Казалось бы, тривиальная задача: CRUD для сотрудников. Но в процессе анализа выяснилось, что существует бизнес‑правило: при увольнении сотрудника его данные не удаляются (по закону их нужно хранить 75 лет), а переходят в архив. Но самое страшное — при увольнении руководителя автоматически должна перестраиваться иерархия подчинения для тысяч сотрудников, и все их зарплатные контракты должны быть переподписаны новым руководителем.
Если в этой задаче применять проектирование сервиса по принципу «сценарий транзакции», то метод terminateEmployee разросся бы до невероятных размеров, пытаясь за одну синхронную операцию обновить все связанные сущности, включая те, что лежат в других микросервисах. Система бы просто упала под нагрузкой.
Как можно выйти из ситуации? Здесь возможно применить подход с агрегатами и событиями:
Агрегат
EmployeeProfileотвечает только за свой статус. При увольнении он переходит в статусTERMINATEDи генерирует событиеEmployeeTerminated.Отдельный агрегат
OrgStructure(оркестратор) подписывается на это событие.Получив событие, Saga
ReassignSubordinatesSagaзапускает процесс переподчинения. Каждый шаг этой Saga обновляет ровно один агрегат (другого сотрудника), и так до тех пор, пока всех подчиненных не переведут.
Итог: вместо одной монструозной транзакции, которая бы заблокировала базу на 30 секунд, получаем серию быстрых, асинхронных операций. Система будет оставаться отзывчивой при любых и даже при массовых увольнениях (например, при закрытии целого филиала).
Нефункциональные требования и тестирование
Хороший архитектор не останавливается на функциональной схеме. Он задает вопросы про производительность, доступность и безопасность. Для нашего HR‑сервиса мы определили следующие NFR:
Производительность: операция найма сотрудника должна занимать < 200 мс (время отклика для пользователя), несмотря на то, что за кулисами запускается Saga из 3 шагов.
Атомарность данных: событие должно быть доставлено хотя бы один раз (at‑least‑once), следовательно, все потребители событий должны быть идемпотентными.
Аудитинг: каждое изменение агрегата, инициированное админом, должно логироваться с указанием
who, when, what.
Прописывая эти требования на этапе проектирования, мы закладываем архитектуру. Например, требование к производительности заставило нас сделать EmployeeProfile полностью изолированным от внешних синхронных вызовов. Все интеграции — асинхронные.
Заключение
Проектирование бизнес‑логики в микросервисной архитектуре — это всегда баланс между прагматизмом и инженерией. Вы можете начать с простых сценариев транзакций, и если бизнес‑логика остается CRUD‑подобной, это может быть оправдано. Но как только появляются сложные правила, инварианты и требования к масштабируемости, без DDD и агрегатов не обойтись.
Хороший системный архитектор учит не столько инструментам, сколько мышлению. Инструменты меняются, но принципы управления сложностью остаются неизменными. Агрегаты, доменные события и ограничения на транзакции — это те самые принципы, которые позволяют нам не бояться изменений и уверенно масштабировать систему.
Если этот разбор был полезен, приходите обсуждать архитектуру в комментариях. Какие подходы к организации бизнес‑логики используете вы? Сталкивались ли с проблемой размытых границ агрегатов в своих проектах?
В OTUS есть несколько курсов по архитектуре, на которых можно серьезно прокачать свои скилы в этом направлении: «Архитектор программного обеспечения», «Микросервисная архитектура», «Domain Driven Design и асинхронная архитектура»

Микросервисы усложняют не код, а принятие решений: где границы, как сохранить целостность данных и не превратить систему в хаос — бесплатные уроки ниже как раз об этом. Приходите, чтобы познакомиться с преподавателями курсов и задать свои вопросы:
14 апреля, 20:00. «Влияние нефункциональных требований на архитектуру». Записаться
15 апреля, 20:00. «Джон, которого нет: Как микросервисы убивают целостность данных и что с этим делать системному аналитику». Записаться
