Форум программистов, компьютерный форум, киберфорум
ArchitectMsa
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Роль Domain-Driven Design в современных архитектурах

Запись от ArchitectMsa размещена 25.09.2025 в 20:41
Показов 2873 Комментарии 0

Нажмите на изображение для увеличения
Название: Роль Domain-Driven Design в современных архитектурах.jpg
Просмотров: 286
Размер:	253.0 Кб
ID:	11213
Шесть лет назад я впервые столкнулся с тем, что впоследствии стало моим худшим кошмаром — монолитным приложением на два с половиной миллиона строк кода. Десятки разработчиков годами вносили изменения, и система превратилась в неуправляемого монстра. Я помню, как зашел в комнату, где архитектор пытался нарисовать на доске схему взаимосвязей модулей, и честно — она напоминала карту метро в час пик, нарисованную пьяным художником.

Знакомая картина, не так ли? Если вы хоть раз работали с большим проектом, вы наверняка видели, как хорошо задуманная архитектура превращается в запутанный клубок зависимостей. Технический долг нарастает как снежный ком, и вскоре команда тратит 80% времени на поддержание существующей функциональности вместо создания новой ценности.

Я работал с финтех-системой, которая обрабатывала миллиарды транзакций в месяц. Казалось бы, крутая система! Но когда нужно было добавить новый тип платежа, я потратил три недели только на то, чтобы разобраться, где именно нужно вносить изменения. Тогда я понял одну простую истину: архитектурная деградация — это тихий убийца проектов.

Симптомы разрушения архитектуры



Как распознать, что ваш код превращается в спагетти? Существует несколько явных признаков:

1. Страх изменений. Когда разработчики боятся трогать определённые части кода, потому что "оно как-то работает" — это первый тревожный звоночек. Помню, в том проекте у нас был модуль расчета комиссий, к которому никто не хотел прикасаться. "Прокляый" модуль, как мы его называли, имел на своём счету три сломанных релиза.
2. Каскадные изменения. Вносите правку в одном месте — ломается десять других. Этот эффект домино возникает, когда бизнес-логика размазана по всей системе без четких границ. В одном из проектов мы даже ввели "коэфициент распространения" — число модулей, которые приходится менять при внедрении новой фичи. Если он превышал 3, значит с архитектурой что-то не так.
3. Дублирование кода и логики. Разработчики не могут найти существующую реализацию или боятся её использовать, поэтому создают новую. Мне приходилось видеть четыре разные реализации одного и того же алгоритма расчёта НДС, разбросанные по разным модулям.
4. Нарушение принципа единой ответственности. Классы, которые делают всё и сразу — от валидации ввода до отправки email-уведомлений. Как-то я наткнулся на класс OrderProcessor размером в 8000 строк с 54 методами. Это был такой Франкенштейн, что даже его создатель уже не понимал, как он работает.
5. Круговые зависимости. Модуль A зависит от B, который зависит от C, который зависит от A. Замкнутый круг, в котором любое изменение может вызвать неожиданные последствия.

Как правильно приготовить DDD (domain-driven design)
Достался проект, который изначально планировался как DDD, но ребята которые его делали до меня...

Паттерн Domain Model (Модель области определения)
Есть у кого нибудь нормальное описание,и рабочие примеры данного Паттерна?))

Срочно Паттерн Domain Model (Модель области определения) КОд С++
Очень срочно нужен любой пример кода данного Паттерна.Прошу помогите.:(

ViewModel. Обертки поверх Domain Model для передачи в пользовательский интерфейс. Best practice
Вопрос знатокам проектирования 3-layer и n-layer приложений. Встал вопрос о передачи не самой...


Как измерить беспорядок в коде



Признавая проблему, важно уметь её квантифицировать. Существуют инструменты и метрики, которые помогают оценить степень архитектурной деградации:

Цикломатическая сложность — измеряет количество линейно независимых путей через код. Когда я проанализировал наш "проклятый" модуль комиссий, его цикломатическая сложность была 247, при рекомендуемом максимуме 10-15.
Связность и связанность — показатели того, насколько модули зависят друг от друга. Высокая связность внутри модуля и низкая связанность между модулями — идеальное сочетание, которое редко встречается в устаревших системах.
Количество зависимых изменений — сколько модулей нужно менять вместе. Этот показатель четко указывает на нарушения в границах доменов.
Покрытие тестами — не просто процент покрытия, а возможность тестировать компоненты изолированно. Если для тестирования одного класса вам нужно поднимать половину системы — это явный сигнал проблем.

В одном из моих проектов мы разработали собственную метрику "коэффициент путаницы" — отношение количества классов, затронутых при изменении, к количеству бизнес-требований. Чем выше коэффициент, тем хуже структурирована система. Я использовал инструменты вроде Structure101, SonarQube и JArchitect для анализа Java-кодбазы. Результаты часто оказывались шокирующими для команды — графики зависимостей напоминали схемы электропроводки древнеегипетских пирамид, если бы таковые существовали.

Но самая ценная метрика — это время. Время, которое требуется новому разработчику, чтобы понять, как внести изменения. Время, необходимое для реализации новой функциональности. Время поиска и исправления ошибок. Когда эти показатели растут, значит, ваша архитектура деградирует. Помню, как в одном проекте проверили, сколько времени нужно новому разработчику, чтобы добавить простую валидацию в форму. Ответ — два дня, при том что сама задача занимала 10 минут. Оставшееся время уходило на понимание, где именно нужно вносить изменения и как не сломать существующую логику.

Все эти симптомы указывают на фундаментальную проблему — отсутствие четкой модели предметной области и размытые границы между различными частями системы. И здесь на сцену выходит Domain-Driven Design как способ структурировать сложность и вернуть системе управляемость.

DDD как философия, а не набор паттернов



Нажмите на изображение для увеличения
Название: Роль Domain-Driven Design в современных архитектурах 2.jpg
Просмотров: 127
Размер:	148.1 Кб
ID:	11214

Когда я впервые услышал о Domain-Driven Design, я думал, что это просто еще один модный набор шаблонов проектирования. «Агрегаты, сущности, объекты-значения... ну да, все это было в учебнике по объектно-ориентированному программированию». Мне потребовалось несколько провальных проектов, чтобы понять — DDD это не про код, это про мышление.

Доменная модель против анемичных сущностей



Большинство современных Java-приложений страдают от синдрома анемичной доменной модели. Помню свой типичный код несколько лет назад:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Order {
    private Long id;
    private List<OrderItem> items;
    private Customer customer;
    private BigDecimal totalAmount;
    
    // Геттеры и сеттеры для всех полей
}
 
public class OrderService {
    public void processOrder(Order order) {
        // Вся бизнес-логика здесь
        calculateTotal(order);
        validateOrder(order);
        saveOrder(order);
        notifyCustomer(order);
    }
}
Выглядит знакомо? Такой подход кажется логичным: сущности хранят данные, сервисы обрабатывают их. Но в результате получаем "тупые" объекты данных и "умные" сервисы, которые со временем разрастаются до неуправляемых размеров.
В DDD же доменная модель — это живой организм, где объекты содержат не только данные, но и поведение:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Order {
    private OrderId id;
    private Set<OrderItem> items;
    private CustomerId customerId;
    private OrderStatus status;
    
    public void addItem(Product product, int quantity) {
        validateProductAvailability(product, quantity);
        OrderItem item = new OrderItem(product.getId(), quantity, product.getCurrentPrice());
        this.items.add(item);
    }
    
    public void place() {
        validateMinimumOrderAmount();
        this.status = OrderStatus.PLACED;
        DomainEvents.publish(new OrderPlacedEvent(this.id));
    }
    
    private void validateMinimumOrderAmount() {
        if (calculateTotal().isLessThan(Money.of(10, Currency.USD))) {
            throw new MinimumOrderAmountNotMetException();
        }
    }
    
    public Money calculateTotal() {
        return items.stream()
                .map(OrderItem::calculatePrice)
                .reduce(Money.zero(Currency.USD), Money::add);
    }
}
Разница фундаментальна. Во втором случае Order — это не просто мешок с данными, а полноценная бизнес-сущность с внутренними правилами и поведением. Бизнес-инварианты (например, минимальная сумма заказа) защищены самим объектом.

Bounded Context — ключ к разделению ответственности



Один из самых мощных концептов DDD — это ограниченный контекст (Bounded Context). Я помню, как мучился с понятием "Пользователь" в крупной CRM-системе. В одном модуле пользователь был клиентом с историей покупок, в другом — контактным лицом организации, в третьем — получателем маркетинговых рассылок. Попытка создать единую модель "Пользователя", удовлетворяющую всем требованиям, приводила к монструозным классам со сотнями свойств и методов. Решение пришло с пониманием, что в разных частях системы термин "Пользователь" имеет разное значение.

Ограниченный контекст устанавливает чёткие границы, внутри которых модель и язык имеют конкретное, согласованное значение. Вместо одного гигантского "Пользователя" мы получаем:
  • Customer в контексте продаж,
  • ContactPerson в контексте управления взаимоотношениями,
  • Subscriber в контексте маркетинга.

Каждый контекст имеет свою модель, свои правила и даже свою базу данных, если необходимо.

Единый язык — мост между разработчиками и экспертами



Одно из самых ценных открытий, которое я сделал, внедряя DDD — это важность единого языка (Ubiquitous Language). Сколько раз вы сталкивались с ситуацией, когда бизнес говорит о "проводке", а разработчики о "транзакции"? Или когда в коде есть PaymentProcessor, а бизнес говорит "клиринговая система"?

Такие расхождения в терминологии могут казаться незначительными, но они создают постоянные недопонимания и ошибки. В одном банковском проекте мы тратили часы на совещания, пытаясь понять, почему требования не соответствуют реализации, пока не осознали, что говорим на разных языках. Решение оказалось простым, но требовало дисциплины: мы создали глоссарий домена, где каждый термин имел чёткое определение, согласованное между разработчиками и экспертами. Этот глоссарий стал нашей библией — термины из него использовались и в коде, и в документации, и в обсуждениях.

Java
1
2
3
4
5
6
7
8
9
// Вместо
public class PaymentProcessor {
    public void processTransaction(Transaction transaction) { ... }
}
 
// Мы писали
public class ClearingSystem {
    public void processPosting(Posting posting) { ... }
}
Кажется очевидным, но эффект был потрясающим — количество недопониманий резко сократилось, а скорость разработки выросла.

Event Storming — выявление доменных событий и границ



Но как определить границы контекстов? Как выявить единый язык? Один из самых эффективных инструментов, которые я использовал — это Event Storming.

В прошлом году мы запускали новую платформу для страховой компании. Вместо того, чтобы начинать с проектирования баз данных или UML-диаграмм, мы собрали в одной комнате разработчиков, аналитиков, экспертов домена и даже конечных пользователей. Вооружившись стикерами разных цветов и большой стеной, мы начали моделировать бизнес-процессы через события.

Оранжевые стикеры представляли доменные события ("Полис оформлен", "Заявление на выплату подано"), синие — команды, которые вызывают эти события, желтые — участников процесса, фиолетовые — проблемы и вопросы. Через несколько часов хаотичного наклеивания и перемещения стикеров начала проявляться структура. Мы увидели, как события группируются вокруг определенных бизнес-процессов и сущностей. Стало ясно, где проходят естественные границы контекстов — например, контекст андеррайтинга четко отделялся от контекста урегулирования убытков.

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

Strategic Design — картографирование сложности



После проведения Event Storming мы получили представление о бизнес-процессах и естественных границах нашего домена. Но как организовать взаимодействие между выявленными ограниченными контекстами? Здесь на помощь приходит стратегическое проектирование и карты контекстов (Context Maps). Помню, как в одной телеком-компании мы пытались интегрировать систему биллинга с CRM-платформой. Без понимания отношений между этими контекстами мы постоянно сталкивались с конфликтами моделей и размыванием ответственности.

Карта контекстов явно определяет взаимотношения между ограниченными контекстами. Она показывает, кто с кем взаимодействует и на каких условиях. Основные типы отношений, которые я регулярно использую:

1. Партнерство (Partnership) — контексты разрабатываются в тесной координации, с общими целями. Это идеальные отношения, но требующие постоянной синхронизации команд.
2. Общее ядро (Shared Kernel) — контексты используют общую часть модели. Например, в том телеком-проекте мы выделили общую модель "Тарифный план", которую использовали и биллинг, и CRM.
3. Заказчик-Поставщик (Customer-Supplier) — восходящий контекст (поставщик) предоставляет сервисы нисходящему (заказчику). Ключевой момент — договоренности о предоставляемом API и ответственность поставщика перед заказчиком.
4. Конформист (Conformist) — нисходящий контекст вынужден принять модель восходящего без возможности влиять на неё. Часто возникает при работе с унаследованными системами или вендорскими решениями.
5. Предохранительный слой (Anti-Corruption Layer) — специальный слой трансляции между контекстами, защищающий один контекст от влияния модели другого. Это мой любимый паттерн при работе со старыми системами — он позволяет сохранить чистоту новой модели.

Визуализация этих отношений в виде карты дает колоссальное преимущество. Однажды в проекте для страховой компании такая карта контекстов выявила, что мы пытались построить конформистские отношения с системой, которая сама находится в активной разработке. Мы изменили подход на партнерство, синхронизировали команды, и это спасло нас от месяцев переделок.

Domain Services против Application Services



Еще одна частая путаница, с которой я сталкивался в Java-проектах — это размытая граница между доменными и приложенными сервисами. Многие разработчики складывают всю логику в классы с суффиксом Service без ясного понимания ответствености. Когда я консультировал крупный e-commerce проект, там был монструозный OrderService с более чем 50 методами и зависимостями от репозиториев, внешних систем, кэшей и т.д. Такой подход делает код непонятным и неподдерживаемым.

Используя принципы DDD, мы разделили ответственность:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Доменный сервис - содержит только бизнес-логику
public class OrderDiscountService {
    public Money calculateDiscount(Order order, CustomerLoyaltyLevel loyaltyLevel) {
        // Чистая доменная логика без внешних зависимостей
        if (order.containsPromotionalItems() && loyaltyLevel.isVIP()) {
            return order.calculateTotal().multiply(0.15);
        }
        // ... другие бизнес-правила
        return Money.zero(order.getCurrency());
    }
}
 
// Сервис приложения - оркестрирует бизнес-процесс
public class OrderApplicationService {
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final OrderDiscountService discountService;
    private final PaymentGateway paymentGateway;
    private final EventPublisher eventPublisher;
    
    public void placeOrder(PlaceOrderCommand cmd) {
        Order order = orderRepository.findById(cmd.getOrderId());
        Customer customer = customerRepository.findById(cmd.getCustomerId());
        
        Money discount = discountService.calculateDiscount(order, customer.getLoyaltyLevel());
        order.applyDiscount(discount);
        
        order.place();
        orderRepository.save(order);
        
        paymentGateway.requestPayment(order);
        eventPublisher.publish(new OrderPlacedEvent(order.getId()));
    }
}
Доменные сервисы содержат только бизнес-логику, которая не вписывается естественно в сущности или объекты-значения. Они не имеют состояния и не зависят от инфраструктуры. Сервисы приложения, напротив, оркестрируют взаимодействие между доменными объектами и инфраструктурными службами. Такое разделение делает код более понятным и тестируемым. Доменные сервисы можно тестировать изолированно, без мокирования внешних зависимостей.

Context Mapping Patterns на практике



Давайте рассмотрим, как реализуются отношения между контекстами в реальном проекте. Недавно мой коллега разрабатывал систему управления медицинской клиникой, где были выделены контексты расписания, пациентов и финансов. Между контекстом расписания и пациентов были установлены отношения "Общее ядро" — обе команды совместно разработали модель "Прием", которая включала идентификатор пациента, время и тип приема. Между контекстом расписания и финансов реализовали модель "Поставщик-Потребитель". Контекст расписания генерировал события о назначенных и выполненных приемах, которые контекст финансов использовал для выставления счетов.

А вот старая унаследованная лабораторная система требовала интеграции через "Предохранительный слой". Она имела свою модель пациентов и результатов анализов, кторая конфликтовала с нашей. Вместо прямой интеграции мы создали ACL (Anti-Corruption Layer):

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LaboratorySystemAdapter {
    private final LegacyLabApi legacyApi;
    
    public void sendPatientData(Patient patient) {
        // Трансляция нашей модели в формат унаследованной системы
        LegacyPatientDto legacyPatient = new LegacyPatientDto();
        legacyPatient.setPatientCode(patient.getId().toString());
        legacyPatient.setFirstName(patient.getName().getFirstName());
        legacyPatient.setLastName(patient.getName().getLastName());
        // Переводим нашу медкарту в их формат
        legacyPatient.setRecordNum(formatMedicalRecordId(patient.getMedicalRecord().getId()));
        
        legacyApi.registerPatient(legacyPatient);
    }
    
    public List<TestResult> fetchTestResults(PatientId patientId) {
        // Получаем данные из легаси системы и транслируем в нашу модель
        String legacyPatientCode = patientId.toString();
        List<LegacyTestResultDto> legacyResults = legacyApi.getResults(legacyPatientCode);
        
        return legacyResults.stream()
                .map(this::convertToTestResult)
                .collect(Collectors.toList());
    }
    
    private TestResult convertToTestResult(LegacyTestResultDto dto) {
        // Трансляция из их модели в нашу
        // ...
    }
}
Такой подход защитил нашу модель от "заражения" концепциями унаследованой системы и позволил нам сохранить чистоту домена.

Однажды мне пришлось интегрировать коммерческий CRM-продукт с нашей системой поддержки. Мы решили использовать паттерн "Соответствующие контексты" (Conformist) — наша система полностью принимала модель CRM. Это было осознаное архитектурное решение, поскольку CRM был центральной системой компании, а наше приложение — вспомогательным инструментом.

Тактические паттерны в действии



Если стратегическое проектирование в DDD — это картография и установка границ, то тактические паттерны — это инструменты для создания чистой и выразительной доменной модели внутри этих границ. Я всегда объясняю это так: стратегия помогает разделить лес на участки, а тактика учит, как правильно сажать деревья внутри каждого участка.

Агрегаты — хранители инвариантов



Агрегат — это кластер объектов, которые мы рассматриваем как единое целое с точки зрения изменения данных. Каждый агрегат имеет корень (root) и границу. Корень агрегата — это единственная сущность, через которую внешний код может получить доступ к внутренним объектам агрегата. Это звучит формально, поэтому давайте посмотрим на пример из банковской системы, с которой я работал:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Account {
    private AccountId id;
    private Money balance;
    private CustomerId ownerId;
    private Set<Transaction> transactions;
    private AccountStatus status;
    
    // Корень агрегата контролирует все изменения
    public void deposit(Money amount, TransactionReference reference) {
        validateAccountIsOperational();
        Transaction transaction = new Transaction(
            TransactionId.generate(), 
            amount, 
            TransactionType.DEPOSIT, 
            reference,
            LocalDateTime.now()
        );
        this.transactions.add(transaction);
        this.balance = this.balance.add(amount);
        DomainEvents.publish(new AccountCredited(this.id, amount, reference));
    }
    
    public void withdraw(Money amount, TransactionReference reference) {
        validateAccountIsOperational();
        ensureSufficientBalance(amount);
        
        Transaction transaction = new Transaction(
            TransactionId.generate(), 
            amount, 
            TransactionType.WITHDRAWAL, 
            reference,
            LocalDateTime.now()
        );
        this.transactions.add(transaction);
        this.balance = this.balance.subtract(amount);
        DomainEvents.publish(new AccountDebited(this.id, amount, reference));
    }
    
    private void ensureSufficientBalance(Money amount) {
        if (balance.isLessThan(amount)) {
            throw new InsufficientBalanceException(id, balance, amount);
        }
    }
    
    private void validateAccountIsOperational() {
        if (status != AccountStatus.ACTIVE) {
            throw new AccountNotOperationalException(id, status);
        }
    }
    
    // Остальные методы...
}
Заметьте, что класс Account контролирует все изменения баланса и управляет внутренней коллекцией транзакций. Внешний код не может напрямую модифицировать список транзакций или изменять баланс — только через методы deposit и withdraw. Это позволяет поддерживать инвариант: баланс всегда должен равняться сумме всех транзакций.

Value Objects — неизменяемые строительные блоки



В приведеном выше примере вы могли заметить тип Money. Это не примитив вроде double, а полноценный Value Object. Value Objects — это объекты, которые не имеют идентичности и полностью определяются своими атрибутами. В отличие от сущностей, которые мы отслеживаем по идентификатору (например, счёт №12345 остаётся тем же счётом, даже если все его атрибуты меняются), Value Objects полностью заменяемы, если имеют одинаковые атрибуты.
Вот как я обычно реализую Value Object для денег:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public final class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    // Конструктор и фабричные методы
    private Money(BigDecimal amount, Currency currency) {
        this.amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
        this.currency = currency;
    }
    
    public static Money of(BigDecimal amount, Currency currency) {
        return new Money(amount, currency);
    }
    
    public static Money of(double amount, Currency currency) {
        return new Money(BigDecimal.valueOf(amount), currency);
    }
    
    public static Money zero(Currency currency) {
        return new Money(BigDecimal.ZERO, currency);
    }
    
    // Бизнес-операции, всегда возвращают новый объект
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
        return new Money(this.amount.subtract(other.amount), this.currency);
    }
    
    public Money multiply(double multiplier) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(multiplier)), this.currency);
    }
    
    public boolean isLessThan(Money other) {
        ensureSameCurrency(other);
        return this.amount.compareTo(other.amount) < 0;
    }
    
    private void ensureSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
    }
    
    // Equals, HashCode и toString
}
Ключевые характеристики Value Objects:
1. Неизменяемость — объект не меняет своего состояния после создания
2. Отсутствие идентичности — два объекта с одинаковыми атрибутами считаются эквивалентными
3. Самовалидация — объект гарантирует свою внутреннюю согласованность

Применение Value Objects дает огромные преимущества: код становится более выразительным, упрощается тестирование, уменьшается количество ошибок. Например, использование Money вместо double автоматически решает проблемы с округлением и операциями между разными валютами.

Репозитории — инфраструктурные ворота в домен



Репозиторий в DDD — это не просто DAO. Репозиторий обеспечивает иллюзию коллекции всех агрегатов определенного типа. Он скрывает детали хранения и предоставляет доменно-ориентированный интерфейс для получения агрегатов.
В одном из проектов мы реализовали репозиторий счетов так:

Java
1
2
3
4
5
6
public interface AccountRepository {
    Account findById(AccountId id);
    void save(Account account);
    void delete(Account account);
    AccountCollection findByOwner(CustomerId ownerId);
}
Заметьте, интерфейс репозитория использует доменную терминологию. Нет упоминаний SQL, ORM или других технических деталей. Это делает доменный код независимым от инфраструктуры.
А вот как выглядела реализация с использованием Spring Data JPA:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Repository
public class JpaAccountRepository implements AccountRepository {
    private final SpringAccountRepository springRepo;
    private final AccountMapper mapper;
    
    public JpaAccountRepository(SpringAccountRepository springRepo, AccountMapper mapper) {
        this.springRepo = springRepo;
        this.mapper = mapper;
    }
    
    @Override
    public Account findById(AccountId id) {
        return springRepo.findById(id.getValue())
                .map(mapper::toDomain)
                .orElseThrow(() -> new AccountNotFoundException(id));
    }
    
    @Override
    public void save(Account account) {
        AccountJpaEntity entity = mapper.toEntity(account);
        springRepo.save(entity);
        // Очистка кэша событий после сохранения
        DomainEvents.clearEventsFor(account);
    }
    
    // Остальные методы...
}
Обратите внимание на маппинг между доменным объектом Account и JPA-сущностью AccountJpaEntity. Это разделение позволяет доменной модели эволюционировать независимо от модели хранения.

Domain Events — коммуникация между агрегатами



Одна из сложностей в DDD — соблюдение принципа, что один транзакционный блок должен изменять только один агрегат. Как тогда координировать изменения между агрегатами? На помощь приходят доменные события.
Доменное событие — это запись о чём-то значимом, что произошло в домене. События позволяют агрегатам косвенно взаимодействовать без прямых зависимостей.
Вот как выглядит типичное доменное событие:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AccountCredited implements DomainEvent {
    private final AccountId accountId;
    private final Money amount;
    private final TransactionReference reference;
    private final LocalDateTime occurredAt;
    
    public AccountCredited(AccountId accountId, Money amount, TransactionReference reference) {
        this.accountId = accountId;
        this.amount = amount;
        this.reference = reference;
        this.occurredAt = LocalDateTime.now();
    }
    
    // Геттеры...
}
А вот простой механизм публикации событий, который я использую в небольших проектах:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DomainEvents {
    private static final ThreadLocal<List<DomainEvent>> events = ThreadLocal.withInitial(ArrayList::new);
    
    public static void publish(DomainEvent event) {
        events.get().add(event);
    }
    
    public static List<DomainEvent> allEvents() {
        return Collections.unmodifiableList(events.get());
    }
    
    public static void clearEvents() {
        events.get().clear();
    }
    
    public static void clearEventsFor(Object aggregateRoot) {
        // Если у вас есть механизм связывания событий с агрегатами
    }
}
В реальных проектах я обычно использую более сложные реализации с применением Spring Application Events или сообщений Kafka/RabbitMQ для распределенных систем.

Specification Pattern — инкапсуляция бизнес-правил



Паттерн Specification (спецификация) позволяет инкапсулировать бизнес-правила в отдельные объекты, которые можно комбинировать и повторно использовать. Я часто применяю его как для запросов к репозиториям, так и для валидации.
Вот базовый интерфейс спецификации:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Specification<T> {
    boolean isSatisfiedBy(T candidate);
    
    default Specification<T> and(Specification<T> other) {
        return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
    }
    
    default Specification<T> or(Specification<T> other) {
        return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
    }
    
    default Specification<T> not() {
        return candidate -> !this.isSatisfiedBy(candidate);
    }
}
А вот примеры конкретных спецификаций для нашей банковской системы:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ActiveAccountSpecification implements Specification<Account> {
    @Override
    public boolean isSatisfiedBy(Account account) {
        return account.getStatus() == AccountStatus.ACTIVE;
    }
}
 
public class MinimumBalanceSpecification implements Specification<Account> {
    private final Money minimumBalance;
    
    public MinimumBalanceSpecification(Money minimumBalance) {
        this.minimumBalance = minimumBalance;
    }
    
    @Override
    public boolean isSatisfiedBy(Account account) {
        return !account.getBalance().isLessThan(minimumBalance);
    }
}
Теперь мы можем комбинировать спецификации для создания сложных правил:

Java
1
2
3
4
5
6
7
Specification<Account> eligibleForCreditSpec = new ActiveAccountSpecification()
        .and(new MinimumBalanceSpecification(Money.of(1000, Currency.getInstance("USD"))))
        .and(new AccountAgeSpecification(Period.ofMonths(6)));
 
List<Account> eligibleAccounts = accountRepository.findAll().stream()
        .filter(eligibleForCreditSpec::isSatisfiedBy)
        .collect(Collectors.toList());
Преимущество этого подхода в том, что бизнес-правила становятся явными, их легко тестировать, а комбинирование дает гибкость без дублирования кода.
В одном финтех-проекте мы пошли еще дальше и связали спецификации с поисковыми критериями в репозиториях:

Java
1
2
3
public interface JpaSpecification<T> extends Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
}
Это позволило нам использовать одни и те же бизнес-правила как для фильтрации объектов в памяти, так и для конструирования SQL-запросов через JPA Criteria API.

Интеграция с современными архитектурами



Нажмите на изображение для увеличения
Название: Роль Domain-Driven Design в современных архитектурах 3.jpg
Просмотров: 80
Размер:	103.9 Кб
ID:	11215

Domain-Driven Design прекрасно вписывается в современные архитектурные подходы, и даже больше — я считаю, что именно благодаря возрождению интереса к микросервисам, событийно-ориентированному программированию и реактивным системам, DDD получил второе дыхание. Давайте посмотрим, как DDD интегрируется с популярными архитектурными стилями и какие выгоды мы получаем от такого симбиоза.

Микросервисы и доменные контексты



Я часто слышу вопрос: "Как определить границы микросервисов?" И ответ, который я даю, обычно вызывает "ага-момент": границы микросервисов естественным образом совпадают с границами ограниченных контекстов DDD.
Когда я руководил рефакторингом монолитной системы логистики в микросервисную архитектуру, мы начали не с технической декомпозиции, а с Event Storming сессий. Выявленные ограниченные контексты — управление заказами, маршрутизация, складской учет, биллинг — стали основой для наших микросервисов.

Каждый микросервис:
1. Имел свою доменную модель.
2. Управлял собственными данными.
3. Коммуницировал с другими сервисами через четко определенные контракты.

Это дало нам не только техническую независимость, но и организационные преимущества — команды могли развивать свои сервисы с минимальной координацией.
Вот как выглядел наш процесс идентификации микросервиса:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Ключевые индикаторы, что функциональность заслуживает отдельного микросервиса:
 
// 1. Отдельная доменная модель с уникальными понятиями
public class Route {
private RouteId id;
private List<Waypoint> waypoints;
private Vehicle assignedVehicle;
private RouteStatus status;
 
public void optimize() {
    // Сложная бизнес-логика оптимизации маршрута
    // Не имеет отношения к другим контекстам
}
}
 
// 2. Собственная команда и жизненный цикл
// 3. Отдельная база данных или схема
// 4. Коммуникация с другими сервисами через события
public class RouteOptimized implements DomainEvent {
private final RouteId routeId;
private final List<WaypointDto> optimizedWaypoints;
private final Duration estimatedDuration;
private final Distance totalDistance;
private final LocalDateTime occurredAt;
}
Однако, есть одна ловушка, в которую я видел, как попадают многие команды: чрезмерная декомпозиция. Если вы разбиваете систему на слишком маленькие сервисы, вы получаете "распределенный монолит" — худшее из обоих миров. Компоненты распределены (со всеми недостатками распределенных систем), но при этом тесно связаны (лишая вас преимуществ независимого развертывания).

CQRS и Event Sourcing — естественные компаньоны DDD



Еще одна мощная комбинация — это DDD с Command Query Responsibility Segregation (CQRS) и Event Sourcing. CQRS разделяет модели для чтения и записи, что идеально подходит для сложных доменных моделей.
В одном из проектов для финансовой компании мы применили следующий подход:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Команда для записи - богатая доменная модель
public class Account {
private AccountId id;
private List<Event> changes = new ArrayList<>();
 
public void deposit(Money amount) {
    apply(new MoneyDepositedEvent(id, amount));
}
 
public void withdraw(Money amount) {
    if (calculateBalance().isLessThan(amount)) {
        throw new InsufficientFundsException();
    }
    apply(new MoneyWithdrawnEvent(id, amount));
}
 
private void apply(Event event) {
    changes.add(event);
    applyEvent(event);
}
 
private void applyEvent(Event event) {
    if (event instanceof MoneyDepositedEvent) {
        handleMoneyDeposited((MoneyDepositedEvent) event);
    } else if (event instanceof MoneyWithdrawnEvent) {
        handleMoneyWithdrawn((MoneyWithdrawnEvent) event);
    }
    // ...
}
 
private Money calculateBalance() {
    // Пересчитываем баланс из истории событий
}
}
 
// Запросы - оптимизированная read-модель
@Service
public class AccountQueryService {
private final JdbcTemplate jdbc;
 
public AccountBalanceDto getBalance(String accountId) {
    return jdbc.queryForObject(
        "SELECT account_id, balance, currency FROM account_balances WHERE account_id = ?",
        new Object[]{accountId},
        (rs, rowNum) -> new AccountBalanceDto(
            rs.getString("account_id"),
            rs.getBigDecimal("balance"),
            rs.getString("currency")
        )
    );
}
}
Event Sourcing идет еще дальше, сохраняя не текущее состояние агрегата, а последовательность событий, которые привели к этому состоянию. Это создает полную и аудируемую историю изменений — бесценное свойство для финансовых и регуляторных приложений.

Однако оба подхода имеют свою цену. Event Sourcing требует изменения мышления и добавляет сложности с версионированием событий и производительностью при восстановлении состояния из большого количества событий. CQRS вводит определенную сложность в синхронизацию моделей чтения и записи. Я рекомендую начинать с простой модели и вводить CQRS и Event Sourcing только там, где они действительно решают конкретные бизнес-проблемы.

Hexagonal Architecture и чистые границы



Гексагональная архитектура (она же Ports and Adapters) прекрасно сочетается с DDD, особенно в части защиты доменной модели от внешних зависимостей. Я использовал этот подход в проекте для телеком-компании:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Порт - интерфейс к внешнему миру
public interface BillingSystem {
void chargeCustomer(CustomerId customerId, Money amount, ChargeDescription description);
CustomerBalance getBalance(CustomerId customerId);
}
 
// Адаптер - конкретная реализация
@Service
public class SapBillingAdapter implements BillingSystem {
private final SapClient sapClient;
 
@Override
public void chargeCustomer(CustomerId customerId, Money amount, ChargeDescription description) {
    SapChargeRequest request = new SapChargeRequest();
    request.setCustomerNumber(customerId.toString());
    request.setAmount(amount.getAmount().doubleValue());
    request.setCurrency(amount.getCurrency().getCurrencyCode());
    request.setDescription(description.getText());
    
    sapClient.executeCharge(request);
}
 
@Override
public CustomerBalance getBalance(CustomerId customerId) {
    SapBalanceResponse response = sapClient.getCustomerBalance(customerId.toString());
    return new CustomerBalance(
        Money.of(response.getBalanceAmount(), Currency.getInstance(response.getCurrencyCode())),
        response.getLastUpdate()
    );
}
}
 
// Сервис домена, использующий порт, не зависит от конкретной реализации
public class ServiceActivationService {
private final BillingSystem billingSystem;
 
public ServiceActivationService(BillingSystem billingSystem) {
    this.billingSystem = billingSystem;
}
 
public void activateService(ServiceId serviceId, CustomerId customerId) {
    Service service = serviceRepository.findById(serviceId);
    
    if (service.requiresInitialPayment()) {
        billingSystem.chargeCustomer(
            customerId,
            service.getActivationFee(),
            new ChargeDescription("Service Activation: " + service.getName())
        );
    }
    
    service.activate();
    serviceRepository.save(service);
}
}
Этот подход дает нам несколько преимуществ:
1. Доменная логика не зависит от внешних систем,
2. Мы можем легко тестировать бизнес-логику, заменяя реальные адаптеры на тестовые,
3. При изменении внешней системы мы меняем только соответствующий адаптер.

Saga Pattern и распределенные транзакции



При разделении приложения на отдельные сервисы возникает проблема: как обеспечить согласованность данных при операциях, затрагивающих несколько сервисов? Классические ACID-транзакции уже не работают. Здесь на помощь приходит паттерн Saga — последовательность локальных транзакций, где каждая транзакция публикует событие, запускающее следующую, и имеет компенсирующую операцию для отката в случае сбоя.
В e-commerce проекте мы реализовали процесс оформления заказа как сагу:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class OrderSaga {
@Autowired
private CommandGateway commandGateway;
 
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderPlacedEvent event) {
    String paymentId = UUID.randomUUID().toString();
    
    // Ассоциируем этот платеж с текущей сагой
    associateWith("paymentId", paymentId);
    
    // Отправляем команду в микросервис платежей
    commandGateway.send(new ProcessPaymentCommand(
        paymentId,
        event.getOrderId(),
        event.getAmount()
    ));
}
 
@SagaEventHandler(associationProperty = "paymentId")
public void handle(PaymentCompletedEvent event) {
    // Отправляем команду в микросервис инвентаря
    commandGateway.send(new ReserveInventoryCommand(
        event.getOrderId(),
        event.getOrderItems()
    ));
}
 
@SagaEventHandler(associationProperty = "orderId")
public void handle(InventoryReservedEvent event) {
    // Отправляем команду в микросервис доставки
    commandGateway.send(new ScheduleDeliveryCommand(
        event.getOrderId(),
        event.getShippingAddress()
    ));
}
 
@SagaEventHandler(associationProperty = "orderId")
public void handle(DeliveryScheduledEvent event) {
    commandGateway.send(new CompleteOrderCommand(event.getOrderId()));
    // Завершаем сагу
    end();
}
 
// Компенсирующие транзакции для отката
@SagaEventHandler(associationProperty = "paymentId")
public void handle(PaymentFailedEvent event) {
    // Отменяем заказ
    commandGateway.send(new CancelOrderCommand(event.getOrderId()));
    end();
}
 
@SagaEventHandler(associationProperty = "orderId")
public void handle(InventoryReservationFailedEvent event) {
    // Отменяем платеж
    commandGateway.send(new RefundPaymentCommand(
        event.getOrderId(),
        event.getPaymentId()
    ));
    
    // Отменяем заказ
    commandGateway.send(new CancelOrderCommand(event.getOrderId()));
    end();
}
}
Саги сложнее прямых транзакций, но дают нам возможность обеспечить согласованность данных в распределенной системе, сохраняя при этом автономность отдельных сервисов.

Database per Service и целостность данных



В мире микросервисов принцип "один сервис — одна база данных" становится стандартом. Каждый ограниченный контекст получает свое собственное хранилище, соответствующее его потребностям. Но это создает новые вызовы. В монолите мы полагались на ограничения внешних ключей для поддержания ссылочной целостности. В распределенной системе нам нужны другие подходы:

1. Денормализация и дублирование данных — каждый сервис хранит копии данных, необходимых ему для работы.
2. Eventual consistency — согласованность достигается не мгновенно, а со временем.
3. Интеграция через события — сервисы обновляют свои локальные копии данных в ответ на доменные события.

Это совершенно другой способ мышления о данных, и он требует тесного взаимодействия с бизнесом для определения допустимых компромиссов в части согласованности.

Подводные камни и антипаттерны



Как и любой мощный инструмент, Domain-Driven Design может не только помочь, но и навредить, если применять его неправильно. За годы работы с DDD я насмотрелся на множество проектов, где благие намерения превращались в архитектурный кошмар. Давайте поговорим о типичных ловушках, в которые попадают команды при внедрении DDD, и как их избежать.

Переусложнение и избыточная абстракция



Одна из самых распространенных ошибок — это чрезмерное увлечение абстракциями и паттернами. Я помню проект автоматизации логистики, где команда так увлеклась "чистотой" DDD, что создала целых пять слоев абстракций для простейшей операции. Что-то вроде:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Слой 1: Доменные объекты
public class Shipment {
    private ShipmentId id;
    private Weight weight;
    // ...
}
 
// Слой 2: Спецификации
public class ValidShipmentSpecification implements Specification<Shipment> {
    // ...
}
 
// Слой 3: Доменные сервисы
public class ShipmentValidationService {
    private final ValidShipmentSpecification specification;
    // ...
}
 
// Слой 4: Прикладные сервисы
public class ShipmentApplicationService {
    private final ShipmentValidationService validationService;
    private final ShipmentRepository repository;
    // ...
}
 
// Слой 5: API
public class ShipmentController {
    private final ShipmentApplicationService service;
    // ...
}
В итоге, чтобы добавить один параметр к отправке, нужно было менять код в пяти разных местах! Команда потратила три месяца на создание "идеальной архитектуры", а затем еще шесть — на бесконечную борьбу с ней.

Мой совет: начинайте просто. DDD — это не догма, а набор инструментов. Используйте только те части, которые решают конкретные проблемы вашего домена. Лучше простая модель, которая работает и развивается, чем идеальный академический пример, который никто не может поддерживать.

Анемичная доменная модель



Обратная крайность — это анемичная модель, где объекты домена превращаются в простые контейнеры данных без поведения. Это особенно распространено в командах, которые приходят из мира процедурного программирования или традиционной трехуровневой архитектуры.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Анемичная модель
public class Order {
    private String id;
    private List<OrderItem> items;
    private BigDecimal total;
    private OrderStatus status;
    
    // Геттеры и сеттеры, никакой логики
}
 
public class OrderService {
    public void placeOrder(Order order) {
        validateOrder(order);
        calculateTotal(order);
        order.setStatus(OrderStatus.PLACED);
        orderRepository.save(order);
        notificationService.notifyCustomer(order);
    }
    
    private void validateOrder(Order order) {
        // Валидация, которая должна быть в Order
    }
    
    private void calculateTotal(Order order) {
        // Расчеты, которые должны быть в Order
    }
}
Я работал с системой управления рисками, где вся бизнес-логика была вынесена в огромные сервисы, а сущности использовались только как структуры данных. В результате логика размазывалась по разным сервисам, дублировалась и постоянно нарушалась. Нам потребовалось полгода рефакторинга, чтобы вернуть логику туда, где ей место — в доменные объекты.

Когнитивная нагрузка и сложность моделей



Существует интересная теория когнитивной нагрузки, которая гласит, что человеческий мозг может одновременно обрабатывать ограниченное количество сложных концепций. Это напрямую относится к проектированию доменных моделей.
Я видел проекты, где архитекторы, увлеченные элегантностью своих моделей, создавали настолько сложные абстракции, что даже опытные разработчики не могли их понять без длительного погружения. Например, в одной банковской системе концепция "транзакции" была разбита на 12 разных классов с глубокими иерархиями наследования. Теоретически это позволяло описать любой возможный сценарий, но на практике внедрение новых типов транзакций превращалось в головоломку.

Простой тест: если новому разработчику требуется больше недели, чтобы начать продуктивно работать с вашей доменной моделью — скорее всего, она слишком сложна. Стремитесь к модели, которая достаточно выразительна для решения бизнес-задач, но при этом понятна без многостраничной документации.

Проблемы производительности с богатыми моделями



Богатые доменные модели с множеством связей между объектами могут создавать серьезные проблемы с производительностью, особенно при работе с реляционными базами данных. В одном из проектов мы столкнулись с классической проблемой N+1 запросов. Загрузка заказа с его позициями, информацией о клиенте, истории статусов и платежах генерировала десятки запросов к базе данных:

Java
1
2
3
4
5
6
7
8
9
Order order = orderRepository.findById(orderId);
// Неявно загружает список items
for (OrderItem item : order.getItems()) {
    // Неявно загружает product для каждого item
    Product product = item.getProduct();
    // Неявно загружает category для каждого product
    Category category = product.getCategory();
    // ...
}
Существуют разные стратегии решения:
1. Использование CQRS — разделение моделей для записи и чтения
2. Настройка ленивой/жадной загрузки — тонкая настройка ORM
3. Денормализация данных — хранение предварительно вычисленных данных
4. Правильное определение границ агрегатов — минимизация объектов, загружаемых вместе

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

Кэширование агрегатов и консистентность



Еще одна распространенная проблема — это кэширование агрегатов. Казалось бы, простое решение для повышения производительности, но в распределенной среде это может привести к проблемам консистентности.
Я работал с высоконагруженной платформой, где разные экземпляры приложения работали с кэшированными копиями одних и тех же агрегатов. Это приводило к гонкам данных — агрегат изменялся в одном экземпляре, но другие продолжали работать с устаревшей версией из кэша.

Наше решение включало:
1. Версионирование агрегатов — каждое изменение увеличивало версию.
2. Оптимистичную блокировку — предотвращение одновременных изменений.
3. Инвалидацию кэша по событиям — публикация событий об изменениях для очистки кэша на других узлах.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
public class Account {
    @Id
    private String id;
    
    @Version
    private Long version;
    
    // ... остальные поля и методы
    
    public void withdraw(Money amount) {
        // логика снятия средств
        DomainEvents.publish(new AccountChangedEvent(this.id));
    }
}
 
// В сервисе
public void handleWithdrawal(WithdrawCommand cmd) {
    try {
        Account account = accountRepository.findById(cmd.getAccountId());
        account.withdraw(cmd.getAmount());
        accountRepository.save(account);
    } catch (OptimisticLockingFailureException e) {
        // Обработка конфликта версий
    }
}
 
// Слушатель событий для инвалидации кэша
@EventListener
public void handleAccountChanged(AccountChangedEvent event) {
    cacheManager.evict("accounts", event.getAccountId());
}

Распределенный монолит — худшее из двух миров



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

В одном из проектов финтех-компании архитектор решил разделить монолит на "микросервисы" по техническим слоям: сервис UI, сервис бизнес-логики и сервис данных. В результате получилась система, которая сочетала все недостатки монолита (тесная связность, необходимость синхронизированного развертывания) со всеми недостатками распределенной архитектуры (сетевые задержки, сложность отладки).

Правильный подход — разделение по ограниченным контекстам, где каждый микросервис полностью отвечает за свой домен, от UI до хранения данных.

Как распознать распределенный монолит? Вот несколько признаков:
1. Изменение в одном сервисе регулярно требует изменений в других.
2. Необходимость синхронного взаимодействия между сервисами.
3. Общая база данных или тесная связь на уровне данных.
4. Невозможность независимого развертывания сервисов.

Если вы обнаружили эти признаки, пришло время пересмотреть границы ваших доменов и, возможно, реорганизовать систему в соответствии с принципами DDD. В одном проекте мы столкнулись с распределенным монолитом, состоявшим из 20+ сервисов, которые невозможно было развертывать независимо. Решение было радикальным: временно вернуться к монолиту, правильно определить ограниченные контексты с помощью Event Storming и только после этого начать декомпозицию, но уже по доменным границам.

Игнорирование контекста внедрения



Одна из ошибок, которую я совершал в начале своей карьеры — это попытка применить все концепции DDD к любому проекту, независимо от его сложности и контекста. Для простых CRUD-приложений полноценное внедрение DDD часто оказывается избыточным. Затраты на создание богатой доменной модели могут не окупиться, если бизнес-логика тривиальна.

Мой подход теперь: адаптировать уровень сложности архитектуры к сложности домена. Для простых частей системы подойдет простая CRUD-модель, для сложных бизнес-процессов — полноценная доменная модель с агрегатами, событиями и всеми соответствующими паттернами.

Поэтапный план миграции legacy-системы на DDD-архитектуру



Нажмите на изображение для увеличения
Название: Роль Domain-Driven Design в современных архитектурах 4.jpg
Просмотров: 75
Размер:	375.5 Кб
ID:	11216

Миграция устаревших систем на DDD-архитектуру — задача не для слабонервных. Я часто сравниваю этот процесс с ремонтом самолета во время полета: нужно заменить устаревшие компоненты, не прерывая работу системы и не разбившись в процессе. Мне приходилось руководить несколькими такими проектами, и я убедился, что ключ к успеху — постепенная трансформация, а не полная перестройка.

Strangler Fig Pattern: удушение монолита



Мой любимый подход к такой миграции — это применение паттерна "Strangler Fig" (Душитель-фикус). Название происходит от тропического фикуса-душителя, который обвивает дерево-хозяина, постепенно вытесняя его и занимая его место. Аналогично мы постепенно обволакиваем унаследованную систему новыми компонентами, построенными в соответствии с DDD-принципами. Я применил этот подход в проекте по модернизации системы автострахования, где монолитное приложение на 15 лет работало без существенных изменений архитектуры. Вместо рискованного полного переписывания мы выбрали пошаговый план:

1. Проанализировать и картографировать существующую систему
- Выявить потоки данных и бизнес-процессы
- Определить "швы" — естественные границы в домене
- Создать тесты для фиксации текущего поведения
2. Внедрить фасад перед существующей системой
- Создать API-слой, перехватывающий запросы к монолиту
- Постепенно перенаправлять часть трафика на новые компоненты

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class PolicyFacade {
private final LegacyPolicySystem legacySystem;
private final NewPolicyDomainService newSystem;
private final FeatureToggleService featureToggles;
 
public PolicyResponse getPolicy(String policyNumber) {
    if (featureToggles.isEnabled("use-new-policy-service", policyNumber)) {
        try {
            return newSystem.getPolicy(policyNumber);
        } catch (Exception e) {
            // Логируем сбой и переходим на легаси-систему
            log.warn("Error in new policy service, falling back to legacy", e);
            return legacySystem.getPolicy(policyNumber);
        }
    }
    return legacySystem.getPolicy(policyNumber);
}
}
3. Выделить первый ограниченный контекст
- Начать с наименее рискованного, но ценного компонента
- Разработать доменную модель для этого контекста
- Реализовать новую функциональность параллельно со старой

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

4. Создать антикоррупционный слой
- Защитить новую модель от "заражения" концепциями старой системы
- Реализовать двунаправленные трансляторы между моделями

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class PolicyAntiCorruptionLayer {
public DomainPolicy toDomainModel(LegacyPolicy legacy) {
    return new DomainPolicy(
        new PolicyId(legacy.getPolicyNumber()),
        new Customer(
            new CustomerId(legacy.getClientId()),
            new PersonName(legacy.getClientFirstName(), legacy.getClientLastName())
        ),
        mapCoverages(legacy.getCoverageList()),
        mapVehicle(legacy.getVehicleData()),
        mapPolicyStatus(legacy.getStatus())
    );
}
 
public LegacyPolicy toLegacyModel(DomainPolicy domain) {
    LegacyPolicy legacy = new LegacyPolicy();
    legacy.setPolicyNumber(domain.getId().getValue());
    legacy.setClientId(domain.getCustomer().getId().getValue());
    // ... остальные преобразования
    return legacy;
}
 
private List<Coverage> mapCoverages(List<LegacyCoverage> legacyCoverages) {
    // Трансляция покрытий из легаси-формата в доменную модель
}
}
5. Постепенно расширять новую систему
- Выделять следующие ограниченные контексты
- Устанавливать отношения между контекстами
- Переключать все больше функций на новую реализацию

Мы использовали функциональные флаги (feature toggles), чтобы постепенно увеличивать долю запросов, обрабатываемых новой системой. Сначала 1% трафика, затем 5%, 20% и так далее, внимательно мониторя метрики производительности и ошибок.

6. Мигрировать данные
- Синхронизировать данные между старой и новой системами
- Постепенно переносить исторические данные
- Валидировать целостность после миграции

Для синхронизации данных мы использовали комбинацию пакетных заданий и обработки событий. Каждое изменение в старой системе публиковалось как событие, которое затем обрабатывалось для обновления новой модели.

7. "Задушить" унаследованную систему
- Постепенно отключать функции в старой системе
- Переключить 100% трафика на новую реализацию
- В конечном итоге вывести унаследованную систему из эксплуатации

Вызовы и уроки



Процесс занял почти два года, но был гораздо менее рискованным, чем полная замена. Самые ценные уроки, которые я извлек:

1. Начинайте с тестов. Перед любыми изменениями создайте всесторонние интеграционные тесты, фиксирующие текущее поведение системы. Они станут вашей страховочной сеткой.
2. Привлекайте экспертов домена. Миграция — отличный момент для восстановления потерянных знаний о домене, особенно если первоначальные разработчики давно ушли.
3. Соблюдайте баланс между рефакторингом и новой разработкой. Легко увлечься "починкой" старого кода, но важно доставлять и новую ценность параллельно с миграцией.
4. Мониторьте показатели успеха. Для нашей миграции мы отслеживали:
- Процент запросов, обрабатываемых новой системой.
- Разницу в производительности между старой и новой реализациями.
- Частоту регрессий и "откатов" к старой системе.
- Количество кода, переведенного на новую архитектуру.
5. Будьте готовы к откату. Всегда имейте план возврата к предыдущей версии, если что-то пойдет не так.

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

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

Рекомендации по внедрению



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

Начинайте с культуры, а не с кода



Внедрение DDD — это в первую очередь культурная трансформация. Я помню, как в одной финтех-компании мы потратили три месяца, пытаясь перестроить монолит по принципам DDD, не меняя процессы работы команды. Результат был предсказуем — красивая архитектура на диаграмах и полный бардак в коде.

Что действительно сработало:

1. Организуйте регулярные моделирующие воркшопы. Соберите разработчиков, аналитиков и предметных экспертов за одним столом. Начните с Event Storming или Domain Storytelling. Важно создать среду, где технари и бизнес говорят на одном языке.
2. Инвестируйте в обучение. Не ожидайте, что команда интуитивно поймет принципы DDD. В одном из моих проектов мы организовали серию внутренних воркшопов, где разработчики по очереди представляли различные аспекты DDD на примерах из нашего домена.
3. Создайте глоссарий домена. Зафиксируйте единый язык в виде живого документа, доступного всей команде. В нашем телекоммуникационном проекте такой глоссарий вывесили на большом плакате прямо в офисе команды — удивительно, как это сократило количество недопониманий.

Выбирайте правильный контекст для старта



Не все части вашей системы одинаково подходят для применения DDD. Я советую начинать с областей, которые:
  • Имеют сложную бизнес-логику (а не просто CRUD);
  • Активно развиваются;
  • Критичны для бизнеса;
  • Имеют выделенных экспертов домена.

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

Начните с малого, но думайте масштабно



Я видел много проектов, где команды пытались сразу перестроить всю систему по принципам DDD. Результат? Бесконечный рефакторинг без видимых улучшений. Лучший подход — небольшие, но законченные изменения:
1. Выделите один ограниченный контекст.
2. Смоделируйте его полностью, с агрегатами, сущностями и событиями.
3. Внедрите эту модель в рабочую систему.
4. Оцените результаты.
5. Масштабируйте успешный опыт.

Измеряйте прогресс



Как узнать, работает ли ваш подход? Я использую несколько метрик:
Скорость изменений — сколько времени занимает внесение новой функциональности.
Частота регрессий — как часто изменения в одной части системы ломают другие.
Понимание системы — насколько быстро новички в команде начинают понимать архитектуру.
Удовлетворенность разработчиков — да, это субъективно, но критически важно!

Избегайте догматизма



DDD — это инструмент, а не религия. В моей практике наиболее успешные внедрения были там, где команды прагматично адаптировали принципы DDD к своему контексту, а не слепо следовали книжным примерам.

В одном из проектов мы сознательно отказались от сложных агрегатов для части системы, которая была по сути CRUD-приложением. Зато для ядра бизнес-логики применили полный набор тактических паттернов DDD. Это разумный компромисс, который позволил нам сосредоточить усилия там, где они приносили максимальную отдачу.

Организационная структура имеет значение



Конвей был прав — структура вашей системы будет отражать структуру вашей организации. Если вы хотите внедрить DDD с ограниченными контекстами, подумайте о формировании команд вокруг этих контекстов. В телеком-проекте мы реорганизовали команды вокруг бизнес-доменов вместо технических слоев. Команда биллинга отвечала за все аспекты своего домена — от API до базы данных. Это невероятно ускорило разработку и улучшило понимание бизнес-процессов.

Не забывайте о техническом совершенстве



DDD сосредоточен на бизнес-домене, но это не значит, что можно игнорировать технические аспекты. Инвестируйте в:
  • Автоматизированные тесты, особенно на уровне агрегатов и доменных сервисов,
  • CI/CD для быстрой обратной связи,
  • Мониторинг и логирование в терминах домена,
  • Документацию, отражающую модель и единый язык

Будьте готовы к эволюции



Доменная модель — это живой организм, который должен развиваться вместе с пониманием домена. Не бойтесь рефакторинга и изменения модели, когда появляются новые знания. Помню, как в проекте управления цепочками поставок мы трижды пересматривали модель "Поставка", по мере того как углублялось наше понимание бизнес-процессов. Каждая итерация делала модель более точной и полезной.

В конечном счете, Domain-Driven Design — это не просто способ организации кода. Это способ мышления о разработке программного обеспечения, где центральное место занимает не технология, а понимание бизнеса, который эта технология обслуживает. И если вы искренне интересуетесь доменом, вы уже на полпути к успеху.

Новый принцип Group By Domain (GBD)
Всем привет! Пытаюсь формализовать принципы чистого (понятного) кода. Хочу обсудить собственную...

Соблюдаю ли я принципы SOLID и используя ли я Design Pattern
как можно проверить соблюдаю ли я принципы SOLID и используя ли я Design Pattern?

Совместимость бд на различных архитектурах ПК и различных ОС
Пишу бд на Access 2007, под ОС Windows 7 c x64, ноутбук - (дальше Ноут). Пытаюсь перенести бд на...

Пролог, списки. Ошибка "Basic domain becomes reference domain: integer"
Понимаю, что ошибка в логике. Не могу понять, как правильно при компиляции программы выдает ошибку...

Адрес вида domain/folder/etc вместо domain/?folder=etc
Здравствуйте. Есть ли способ сделать адресную строку более удобочитаемой, например,...

domain.com и www.domain.com в разных папках
Доброе время суток) Подскажите пожалуйста, как реализовать чтобы domain.com и www.domain.com...

Сайт доступен по www.domain.ru/index.php, но недоступен по www.domain.ru
Здравствуйте, возникла проблема: Сайт доступен по www.domain.ru/index.php, но недоступен по...

The variable is not bound in this clause и Basic domain becomes reference domain
Вот так ошибок нет: ... firlast(L,X,Y):- first(L,X), last(L,Y). first(,X):- X=H....

Ошибки Pow: Domain error и Log10: Domain error
Здравствуйте, возникла проблема, при компиляции программы выдаёт ошибки, указанные в заголовке,...

http://.domain.ru -> http://domain.ru/index.
Как сделать такое преобразование?: http://&lt;name&gt;.domain.ru -&gt; http://domain.ru/index.php?user=$1...

Что означает "Domain parked This domain is managed with easyname.com"
Что такое - домен припаркован? Этот сайт выдал мне такое сообщение: http://virustracker.info/

Windows.Forms.Design or ComponentModel.Design
Интересуюсь разработкой дизайнера форм на С#, есть ли у когонибуть доки или примеры по двум...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Модель здравоохранения 11. Создаём классы Задание и Работник
anaschu 22.05.2026
В AnyLogic каждая заявка и каждый ресурс — это объект определённого класса. Нам нужно создать два класса: Задание (заявка) и Работник (ресурс). Класс Задание В дереве проекта нажимаем правой. . .
Модель здравоохранения 10. Новая модель, смотрим, как добавлять логические блоки, и что писать внутри
anaschu 22.05.2026
Открываем AnyLogic, создаём новый проект. В дереве проекта появляется класс Main — это главный агент, в котором будет жить вся наша логика. Палитра блоков Слева находится палитра. Нас интересует. . .
модель ЗдравоСохранения 9. Новая модель, разбираемся, как ее создавать
anaschu 22.05.2026
В этой серии постов мы построим модель небольшого рабочего коллектива. Сотрудники получают задания, выполняют их, иногда болеют — и мы хотим посчитать, сколько это стоит компании. Метод. . .
[golang] Linked list
alhaos 22.05.2026
Связный список / Linked list Связный список структура данных позволяющая хранить список значений, в отличии от массива в памяти хранится не сплошным куском, а отдельными частями которые ссылаются. . .
[golang] Двоичная куча, min-heap
alhaos 20.05.2026
Двоичная куча Двоичная куча — структура данных, которая всегда держит самый важный элемент наготове. Представьте очередь к хилеру в игре, и очередь из игроков в приоритете те у кого меньше. . .
[golang] Breadth-First Search
alhaos 19.05.2026
BFS (Breadth-First Search) — это базовый алгоритм обхода графа в ширину, который поуровнево исследует все связанные вершины. Он начинает с выбранной точки и проверяет всех соседей, прежде чем. . .
[golang] Алгоритм «Хак Госпера»
alhaos 17.05.2026
Алгоритм «Хак Госпера» Хак Госпера (Gosper's Hack) — алгоритм нахождения следующего по величине числа с тем же количеством установленных бит. Придуман Биллом Госпером в 1970-х, опубликован в. . .
Рисование бинарного древа до 6-го колена на js, svg.
russiannick 17.05.2026
<svg width="335" height="240" viewBox="0 0 335 240" fill="#e5e1bb"> <style> <!]> </ style> <g id="bush"> </ g> </ svg> function fn(){ let rost;/ / высота древа let xx=165,yy=210,w=256;
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru