Всем привет, меня зовут Сергей Прощаев, сегодня предлагаю обсудить тему проектирования. Представьте ситуацию: у вас есть, казалось бы, простой микросервис пользователей. Через полгода в нём 30 таблиц, логика размазана по сервисам как масло по горячему тосту, а на вопрос «почему при подтверждении email падает заказ?» никто не может дать ответ, не открыв IntelliJ и не продравшись через пять часов дебага.

Знакомо? Наверное многие через это проходили. И каждый раз причина была одна: мы не договорились о том, что на самом деле делает система. Мы думали таблицами и классами, а не бизнес-процессами. Именно здесь на сцену выходит Domain-Driven Design (DDD).

Не буду грузить вас теорией Эванса и Вернона страницами. Вместо этого я покажу, как DDD реально помогает не выстрелить себе в ногу, когда проект разрастается.

Проблема, которую DDD решает каждый день (и вы о ней даже не догадываетесь)

Самая большая иллюзия в IT — это прозрачность. Нам кажется, что если мы написали спецификацию, то и разработчик, и заказчик понимают одно и то же.

Нет.

Я видел проект, где слово «Заказ» значило для отдела продаж одно (счёт на оплату), для логистов другое (набор коробок на складе), а для разработчиков третье (строка в БД с флагом is_paid). Когда эти представления сталкивались, система начинала «гореть». Баги не были багами в классическом смысле — просто каждый говорил на своём языке и интерпретировал данные по-своему.

DDD начинается с простой, но гениальной идеи: прекратить перевод с бизнес-языка на технический. Вместо этого мы создаём единый язык (Ubiquitous Language), на котором говорят и директор, и тимлид, и стажёр.

Ubiquitous Language — это не про глоссарий в Confluence

Когда мне говорят «мы завели гугл-таблицу с терминами», я понимаю, что DDD в этой компании нет. Язык — это не мёртвый список слов. Это живая коммуникация. Это когда на стендапе разработчик говорит аналитику: «Слушай, если Заказ аннулирован после отгрузки, мы же должны создать Акт списания?», и аналитик кивает, потому что они оба понимают контекст слов «аннулирован», «отгрузка» и «акт».

К Ubiquitous Language нужно относиться как к коду. Он эволюционирует. Если в разговоре с экспертом выясняется, что «Клиент» и «Плательщик» — это разные роли в разных ситуациях, язык должен это отразить. Меняются названия классов, меняются названия полей, меняются даже названия сервисов.

Практический совет: В следующий раз, когда будете обсуждать новую фичу, записывайте не только задачи, но и ключевые термины, которые использует бизнес. А потом спросите себя: «А в коде эти сущности так же называются?». Если нет — вы только ч��о нашли технический долг, который больнее, чем кривой SQL-запрос.

Bounded Context — спасательный круг для микросервисов

Я большой поклонник микросервисов. Но я ненавижу, когда их режут по техническим причинам: «Выделим сервис оплаты, потому что там будет своя база данных». Это путь в ад!

Единственный адекватный способ нарезать систему — по Bounded Contexts (Ограниченным контекстам). Это как границы государства: внутри одного контекста слово имеет одно значение, за его пределами — может иметь другое.

Вот пример из практики FinTech. В одной команде был Контекст «Кредитный конвейер» и Контекст «Бухгалтерия». И там, и там есть сущность «Клиент» и «Договор». Но в конвейере «Клиент» — это набор скоринговых баллов и скоринг данных, а в бухгалтерии — это ИНН и расчётный счёт. Команда пыталась сделать один общий сервис «Клиент» на всё. Закончилось тем, что бухгалтеры не могли закрыть отчётность, потому что конвейер менял данные клиента в процессе скоринга.

Решение пришло с DDD: команда разделила сервисы. В бухгалтерии — свой «Клиент» (жёсткий, с реквизитами), в конвейере — свой (временный, для расчёта вероятности). Связали их через асинхронные события. Система выдохнула!

Визуально контексты можно изобразить так, как это изображено на рис. 1. Это диаграмма контекстов для нашей гипотетической системы:

Рис. 1 Пример диаграммы контекстов
Рис. 1 Пример диаграммы контекстов

Красота этого подхода в том, что команды могут развивать эти контексты независимо. Меняешь скоринг в конвейере — бухгалтерия не падает, потому что у неё своя копия нужных данных.

Тактические паттерны: из чего строить домен

Когда контексты нарезаны, встаёт вопрос: как организовать код внутри? Тут на помощь приходят Entity, Value Object и Aggregate.

Прошу, забудьте про anemic domain model, когда класс — это просто сеттер/геттер над таблицей в БД. Это не объектно-ориентированное программирование, это работа с данными через красивый фасад.

Value Object — неизменяемая ценность

Самый недооценённый паттерн. Value Object — это объект, который определяется своими атрибутами и не имеет идентичности. Деньги (100 рублей), координаты точки (x, y), Email.

Почему это круто? Потому что он сам проверяет свою валидность и не даёт создать невалидное состояние.

Вот как я обычно показываю разницу новичкам. Вместо того чтобы хранить email как строку:

public class User {
    private String email; // Плохо: может быть null, может быть “fsdfsdf”, 
                          // может быть пустым!
}

Мы создаём Value Object для Email и это исправляет ситуацию:

package ru.otus.dddexample.sharedkernel;

public class Email {
    private final String value;

    public Email(String value) {
        if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Некорректный email: " + value);
        }
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Email email = (Email) o;
        return Objects.equals(value, email.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

Теперь в коде сущности User будет Email email, и я спокоен — туда не попадёт мусор! Вся логика валидации и сравнения в одном месте.

Entity — у кого есть история

В отличие от Value Object, у Entity есть идентификатор, который тянется через всю жизнь объекта. Пользователь может сменить имя, но он останется тем же пользователем, потому что у него есть ID.

Здесь важно не намешать всего в кучу. Entity должен содержать только те данные и ту логику, которая относится к его идентичности и жизненному циклу.

Aggregate — атомарная единица изменения

Самый сложный для понимания, но критически важный паттерн. Агрегат — это граница транзакционной целостности.

Что это значит на практике? Если вы сохраняете Заказ, вы должны сохранить и его Позиции. Если вы удаляете Заказ, позиции должны удалиться. Заказ и его позиции — это один агрегат. Корнем агрегата будет Заказ.

Вот как может выглядеть упрощённый код для Корня агрегата (Aggregate Root):

package ru.otus.dddexample.order.domain;

import ru.otus.dddexample.sharedkernel.Money;
import java.util.ArrayList;
import java.util.List;

public class Order {
    private final OrderId id;
    private List<OrderItem> items;
    private OrderStatus status;

    public Order(OrderId id) {
        this.id = id;
        this.items = new ArrayList<>();
        this.status = OrderStatus.NEW;
    }

    public void addItem(Product product, int quantity) {
        // Бизнес-правило: нельзя добавить товар в подтверждённый заказ!
        if (this.status != OrderStatus.NEW) {
            throw new IllegalStateException("Нельзя изменить подтверждённый заказ");
        }
        this.items.add(new OrderItem(product, quantity));
        // Здесь же можно пересчитать общую сумму
    }

    public Money calculateTotal() {
        return items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(Money.ZERO, Money::add);
    }
    // Геттеры, equals/hashCode по id и тд ...
}

Важно: нельзя ссылаться из одного агрегата на внутренности другого агрегата. Только по ID. Если Заказу нужны данные Пользователя, он хранит userId, а не объект User.

Моделирование — это диалог, а не рисование

Я заметил, что самые успешные команды не те, кто рисует красивые Event Storming диаграммы. А те, кто умеет после этих воркшопов сесть и закодировать домен так, чтобы он отражал реальность.

Моделирование происходит постоянно. Нельзя один раз нарисовать диаграмму классов и успокоиться. Бизнес меняется, и модель должна меняться. Если вы тратите две недели на изменение названия поля в БД, потому что оно захардкожено в 50 местах, — вы неправильно спроектировали домен. В хорошей модели изменения делаются быстро и безболезненно.

История из реального мира: как тушат пожар в монолите

Еще один случай, который хорошо иллюстрирует силу DDD. Монолитное приложение для интернет-эквайринга начало «тормозить» под нагрузкой. Причём не просто тормозить, а падать в моменты пиковых платежей в «чёрную пятницу».

Команда начала оптимизировать SQL, добавлять индексы, кеши. Помогало, но ненадолго. Потом они сели и нарисовали карту контекстов. Оказалось, что в монолите смешаны три разных контекста: «Обработка платежа», «Фрод-мониторинг» и «Уведомления клиента». При каждом платеже синхронно вызывались все три, и самый медленный (фрод-анализ с кучей правил) блокировал ответ платёжному шлюзу.

Было принято нестандартное для решение: не разбивать монолит на микросервисы (это было бы долго), а чётко ограничить контексты в рамках одного приложения. Выделили порты и адаптеры. Фрод-мониторинг сделали асинхронным через внутреннюю очередь. Платеж теперь проходил быстро, а фрод догонял его и в случае чего откатывал транзакцию отдельным компенсирующим действием (Saga).

Производительность выросла в разы, а главное — они смогли это сделать силами одной команды за пару месяцев, не дожидаясь, пока инфраструктура созреет для микросервисов. Это и есть практическая польза от понимания границ.

Заключение

Мы разобрали только вершину айсберга: язык, контексты, агрегаты. В следующих статьях (и на нашем курсе по предметно-ориентированному проектированию (DDD)) мы углубимся в репозитории, доменные события и то, как строить вокруг этого асинхронную архитектуру.

Если вы чувствуете, что ваши проекты тонут в сложности, а коммуникация с бизнесом напоминает испорченный телефон — приходите разбираться с предметно-ориентированным проектированием (DDD) системно. На открытом уроке 18 марта в 20:00 мы как раз будем решать задачу проектирования домена, с учётом всех подводных камней, о которых не пишут в книгах. Участие бесплатно, нужно зарегистрироваться.

Урок пройдет в рамках онлайн-курса «Предметно-ориентированное проектирование (DDD) и асинхронная архитектура».

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