- ТРЕБОВАНИЯ
- КЛАСС ОБЛАСТЕЙ
- ГРАНИЦЫ СМЫСЛА
- ТОПОЛОГИЯ + СВЯЗЬ + ДАННЫЕ ← один узел
- ПАКЕТЫ ┐
- ЛОГИКА │ для каждого модуля
- ИЗОЛЯЦИЯ │ заново
- НАДСТРОЙКИ ┘
- СКВОЗНОЕ
- СВОЙСТВА
- ПЛАТФОРМА
- ФИКСАЦИЯ
- Требования
Решаем: что вообще нужно.
Выбора нет — собираем:
Функциональные требования НФТ в числах: RPS, p99, RPO, RTO, объём данных, рост Ограничения: бюджет, срок, стек, команды, регулятор Требования к целостности: где допустима рассогласованность и на сколько
Выход: документ на 1–2 страницы с числами.
Без чего нельзя идти дальше: без чисел. «Быстро» и «надёжно» не являются требованиями.
- Класс областей
Решаем: сколько церемонии окупится.
Как: выписать 15–30 сценариев → сгруппировать → в каждой группе разложить 5–7 сценариев на шаги → разметить шаги как ПРАВИЛО / ПЕРЕНОС / ВЫЗОВ / РАСЧЁТ → посчитать доли.
Выбираем из:
Класс Условие Что дальше A — Data-centric правил < 15% короткий путь B — Domain-centric правил ≥ 30% полный путь C — Integration-centric вызовы внешних ≥ 30% акцент на шаге 8 D — Compute-centric ценность = расчёт акцент на алгоритме E — Workflow-centric мало правил, много ожиданий во времени оркестратор
Выход: метка на каждую область.
Правило: классов должно быть больше одного. Если вся система получила одну букву — перепроверьте.
- Границы смысла
Решаем: из каких предметных областей состоит система.
Делаем только для областей класса B. Для A и C — глоссарий на полстраницы, без event storming.
Инструменты: Event Storming, Domain Storytelling, разбор глоссария.
Выбираем из — классификация поддоменов:
Тип Признак Стратегия Core конкурентное преимущество лучшие силы, полный DDD Supporting нужно, но преимущества не даёт простые решения Generic решённая задача купить, не писать
Выбираем из — паттерн интеграции на каждую пару контекстов:
Shared Kernel · Customer/Supplier · Conformist · ACL · Open Host Service · Published Language
Дефолт для всего внешнего — ACL.
Выход: список контекстов, глоссарий на каждый, context map.
- Топология + связь + данные
Решаем: сколько исполняемых единиц. Три вопроса решаются вместе, не последовательно.
Выбираем из — топология:
Вариант Когда Монолит прототип, малая команда Модульный монолит дефолт Микросервисы есть независимая причина
Независимые причины разделения — нужна хотя бы одна:
разный профиль нагрузки разные команды с разным циклом релиза разные требования к доступности разный стек регуляторная изоляция данных
Ни одной — модульный монолит.
Выбираем из — коммуникация: REST · gRPC · GraphQL · messaging · WebSockets
Выбираем из — данные: RDBMS · NoSQL · TimeSeries · Graph · polyglot · sharding · replication · caching · event store
Правило владения: одна таблица — один владелец. Шаринг БД между контекстами убивает границы.
Выход: число deployable units, схема связи, карта хранилищ.
- Пакеты
Шаги 4–7 проходятся заново для каждого модуля.
Решаем: чем определён верхний уровень пакетов.
Выбираем из:
Вариант Когда By layer класс A, маленький проект By feature дефолт Modular Monolith (api/ + internal/) модулей > 3 или несколько команд Vertical Slice сценарии сильно разной сложности
Критерий проверки — Screaming: верхний уровень читается как order, billing, shipping, не как controller, service, repository.
Обязательно: ArchUnit или Spring Modulith. Границы без автопроверки исчезают за два спринта.
- Логика
Решаем: где физически живёт логика.
Выбираем из (тройка Фаулера):
Вариант Когда Transaction Script класс A, C Table Module / SQL-проекции отчётность, наборы строк Domain Model класс B
Шкала (не выбор, оценка): Anemic ↔ Rich
Надстройка (поверх любого): Service Layer / Application Service — транзакции, оркестрация, вызов внешних, публикация событий. Бизнес-правил здесь быть не должно.
Инструментарий, если Domain Model + Rich: Entity · Value Object · Aggregate · Repository · Domain Service · Factory · Domain Event
Правила агрегата:
Агрегат = транзакционная граница, одна транзакция = один агрегат Ссылки между агрегатами только по ID Агрегаты маленькие Между агрегатами — итоговая согласованность Границу определяет инвариант, не таблица
Берите всегда, даже в Transaction Script: Value Objects вместо примитивов.
- Изоляция
Решаем: как защитить ядро. Определяется тем, что вышло на шаге 5.
Выбираем из:
Вариант Когда Без изоляции класс A, прототип Layered класс A/C, Transaction Script Hexagonal / Onion / Clean класс B, Rich Domain Model Functional Core логика как преобразование
Выбор между Hexagonal, Onion и Clean — вопрос словаря команды, не архитектуры. Одно семейство.
Проверка: бизнес-правило тестируется без Spring-контекста и без базы. Если нужен @SpringBootTest — изоляции нет.
- Надстройки
Решаем: какие дополнительные разделения. Берутся реактивно, по факту боли.
Надстройка Триггер CQS всегда — бесплатно CQRS ур. 2 (QueryService с прямым SQL) модель чтения разошлась с моделью записи Domain Events in-process модули слиплись Event Sourcing бизнесу нужна история, не состояние
Для событий — выбрать гарантию:
Режим Когда Синхронный в транзакции строгая согласованность AFTER_COMMIT неважные побочные эффекты Outbox дефолт для важных
- Сквозное
Решаем один раз на систему:
Транзакции: @Transactional только в Application Service; транзакция = агрегат Ошибки: домен → 422, сценарий → 404, техническая → 503; домен не знает про HTTP Валидация: формат → адаптер, значение → VO, инвариант → агрегат Тесты: доля тестов, требующих Spring — показатель качества изоляции Идемпотентность, retry, DLQ — обязательно для класса C
- Свойства
Решаем под уже выбранную структуру:
Resilience (таймауты, retry, circuit breaker, bulkhead) · Observability (метрики, логи, трассировки, алерты) · Security-механизмы · Build & Delivery · Compliance
Требования к consistency, нагрузке и безопасности пришли на шаге 0.
- Платформа
Bare-metal · VM · контейнеры · K8s/Nomad · Cloud PaaS
Если платформа задана извне — это было ограничение шага 0, а не решение здесь.
- Фиксация C4: Context, Container, при необходимости Component Dynamic diagram на 2–3 критичных сценария ADR на каждое существенное решение
ADR важнее диаграмм: диаграмма показывает что получилось, ADR — почему так решили и что отвергли. Диаграммы устаревают, ADR нет.
Классификация системы¶
Сколько в этой системе бизнес-правил на единицу данных?
- Data-centric — CRUD
- Domain-centric — бизнес-правила, состояния, инварианты и взаимодействие бизнес-понятий.
- Integration-centric — оркестрация и интеграция внешних систем
- Compute-centric — ценность в результате расчёта
Strategic DDD¶
Границы смысла. Из каких предметных областей состоит система и где между ними границы?
Domain — вся предметная область бизнеса.
- Subdomain — часть проблемы, существует в бизнесе независимо от нас
- Core — конкурентное преимущество
- Supporting — нужно, но преимущества не даёт
- Generic — решённая задача, брать готовое
- Bounded Context — часть решения, граница непротиворечивости модели
- Ubiquitous Language — язык бизнеса, ставший именами классов и методов
- Context Map — связи между контекстами
- Shared Kernel
- Customer / Supplier
- Conformist
- Anticorruption Layer — дефолт для всего внешнего
- Open Host Service
- Published Language
Инструменты:
- Event Storming
- Domain Storytelling
- Разбор глоссария
Bounded Context — явная граница, внутри которой конкретная модель и её язык имеют однозначный смысл. Customer может означать совершенно разные вещи в Sales, Billing, Support и не надо пытаться создать универсальный Customer на 100500 полей в бд, поэтому: граница применимости конкретной модели и Ubiquitous Language, внутри которой термины и правила должны иметь однозначный смысл.
Ubiquitous Language — общий язык разработчиков и domain experts внутри Bounded Context, который используется в разговорах, документации, моделях и коде.
Context Map описывает отношения между Bounded Contexts.¶
Deployment Topology¶
Сколько исполняемых единиц и что оправдывает разделение?
- Монолит
- Модульный монолит — дефолт
- Микросервисы
Независимые причины разделения (нужна хотя бы одна):
- разный профиль нагрузки
- разные команды с разным циклом релиза
- разные требования к доступности
- разный стек
- регуляторная изоляция данных
Communication API¶
Data Architecture¶
Micro Arch: Package Structure¶
Что является основной единицей организации кода?
-
Package by Layer Техническая роль: controller / service / repository / model
-
Package by Feature Бизнес-возможность / предметная область: order / payment / catalog
-
Vertical Slice Конкретный use case: createOrder / cancelOrder / getOrder
Стратегии могут комбинироваться:
feature/
├── api/
├── application/
├── domain/
└── infrastructure/
feature/
├── create/
├── update/
├── get/
└── domain/
Micro Arch: Business Logic¶
Где физически живёт логика внутри модуля?
Выбор из трёх (тройка Фаулера):
- Transaction Script
- Table Module
- Domain Model
-
Transaction Script Идея максимально простая: Каждая бизнес-операция = процедура/метод, который выполняет весь сценарий от начала до конца.
-
Table Module Здесь идея другая: Для каждой таблицы/основной структуры данных существует объект, содержащий бизнес-логику для всех записей этой таблицы.
-
Domain Model Здесь бизнес представляется объектной моделью предметной области.
Возьмём одно правило: Заказ после отправки отменять нельзя.
Transaction Script
Правило находится в процедуре сценария:
public void cancelOrder(long id) {
OrderRow order = repository.findById(id);
if (order.status() == SHIPPED)
throw ...;
repository.setStatus(id, CANCELLED);
}
Table Module
Правило находится в модуле, представляющем операции над orders:
class OrderModule {
public void cancel(long id) {
if (getStatus(id) == SHIPPED)
throw ...;
setStatus(id, CANCELLED);
}
}
Domain Model
Правило принадлежит самому бизнес-объекту:
class Order {
public void cancel() {
if (status == SHIPPED)
throw ...;
status = CANCELLED;
}
}
Поэтому концептуально:
Transaction Script
│
│ бизнес-логика организована
│ вокруг ОПЕРАЦИЙ
│
│ cancelOrder()
│ payOrder()
│ createOrder()
│
▼
Table Module
│
│ бизнес-логика организована
│ вокруг ТАБЛИЦ / НАБОРОВ ДАННЫХ
│
│ OrderModule
│ CustomerModule
│ ProductModule
│
▼
Domain Model
│
│ бизнес-логика организована
│ вокруг ОБЪЕКТОВ ПРЕДМЕТНОЙ ОБЛАСТИ
│
│ Order
│ Customer
│ Money
│ DiscountPolicy
│
▼
Насыщенность модели (шкала, не выбор) - характеристика исключительно Domain Model:
- Anemic ←→ Rich
ANEMIC RICH
│ │
▼ ▼
данные данные
get/set +
│ бизнес-поведение
│ +
│ инварианты
│ +
│ защита состояния
▼ ▼
Order → Order
- id - id
- status - status
- total - total
+ getters/setters + pay()
+ cancel()
+ addItem()
+ calculateTotal()
Оркестрация (надстройка поверх любого из трёх):
Это просто про то что надо выделить сервис и бизнес логику писать в отдельных сервисах. Service Layer -- весь слой сценариев приложения Application Service -- конкретный сервис/use case этого слоя а Domain Service -- это другое! конкретная бизнес-логика внутри domain
- Service Layer, Application Service
Без Service Layer - Контроллер сам делает всё Добавляем Service Layer Мы говорим: Все сценарии приложения вынесем из Controller в отдельный слой.
Инструментарий Rich Domain Model (надстройка):
DDD tactical
- Entity
- Value Object
- Aggregate
- Repository
- Domain Service
- Domain Event
Entity -- доменный объект, который имеет собственную идентичность, жизненный цикл, состояние может меняться, содержит бизнес поведение
public class Order {
private OrderId id;
private OrderStatus status;
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new OrderCannotBeCancelledException();
}
status = OrderStatus.CANCELLED;
}
}
идентичность = то что при одинаковых данных, объекты все равно остаются разными за счет id
Entity в DDD ≠ @Entity в JPA. JPA Entity — persistence-концепция. DDD Entity — бизнес-концепция.
Value Object — доменный объект, который определяется своим значением, а не идентичностью.
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.signum() < 0) {
throw new InvalidMoneyException();
}
}
}
два объекта с одинаковыми значениями означает одно и то же, им не нужен id Value Object особенно полезен вместо primitive obsession. То есть вместо String email, можно Email email Причём Value Object сам защищает корректность Их желательно делать immutable.
Aggregate -- группа Domain Objects, которая является одной границей бизнес-согласованности.
Order
Aggregate Root
│
┌──────────┼──────────┐
▼ ▼ ▼
OrderLine OrderLine DeliveryAddress
Entity Entity Value Object
← один Aggregate →
Сам Order здесь — Aggregate Root. Главное правило: Внешний код изменяет Aggregate через его Root.
Правила проектирования агрегата:
- Агрегат = транзакционная граница
- Одна транзакция меняет один агрегат
- Ссылки между агрегатами — только по ID, не по объекту
- Агрегаты должны быть маленькими
- Согласованность между агрегатами — итоговая (eventual)
- Инвариант определяет границу, а не таблица
Repository — абстракция получения и сохранения Aggregate. Ещё важная DDD-идея: Repository обычно создаётся для Aggregate Root, а не автоматически для каждой Entity.
Domain Service — бизнес-операция, которая относится к Domain, но не принадлежит естественно одной Entity или Value Object.
Domain Service ≠ Application Service
Это ключевой критерий. Допустим, есть сложный расчёт скидки: Расчёт зависит сразу от: Customer, Order, Promotion и нет очевидного объекта, которому естественно принадлежит вся операция. Тогда: DiscountService может быть хорошим Domain Service.
Domain Event — факт, значимый для бизнеса, который уже произошёл в Domain.
Micro Arch: Core Isolation¶
Как отделить бизнес-логику от HTTP, Spring, БД, Kafka, внешних API и других технических деталей?
- Без изоляции
- Layered
- Hexagonal / Onion / Clean — одно семейство, разница в словаре (Одно семейство архитектурных подходов с общей целью и принципом Dependency Inversion, но с различиями в модели слоёв, терминологии и акцентах.)
- Functional Core / Imperative Shell
Определяется тем, что вышло в Business Logic. Проверка: правило тестируется без Spring-контекста и без базы.
Hexagonal / Onion / Clean:
Общая идея у всех:
BUSINESS CORE
↑
abstractions
↑
────────────────────────────────
architectural boundary
────────────────────────────────
↑
implementations
HTTP / DB / Kafka / Framework
Различается акцент:
Hexagonal
→ Ports & Adapters
Onion
→ concentric layers + dependencies inward
Clean
→ Entities / Use Cases / Interface Adapters /
Frameworks & Drivers
Layered Architecture - controller/ service/ domain/ repository/¶
Hexagonal говорит - Бизнес/application core находится внутри, внешний мир взаимодействует с ним через Ports, а технические реализации являются Adapters.
HTTP
│
REST Adapter
│
▼
Inbound Port
│
┌─────────▼─────────┐
│ │
│ APPLICATION │
│ + │
│ DOMAIN │
│ │
└─────────┬─────────┘
│
Outbound Port
│
┌──────────┼─────────┐
▼ ▼ ▼
JPA Kafka Stripe
Adapter Adapter Adapter
Onion Architecture Идея очень близкая, но визуализируется через концентрические слои:
┌────────────────────────────────────┐
│ Infrastructure │
│ │
│ ┌────────────────────────────┐ │
│ │ Application │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Domain │ │ │
│ │ │ │ │ │
│ │ │ Order │ │ │
│ │ │ Money │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────────┘ │
└────────────────────────────────────┘
Главное правило: Зависимости направлены внутрь.¶
Clean Architecture развивает очень похожую идею:
┌─────────────────────────────────────┐
│ Frameworks & Drivers │
│ │
│ ┌─────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Use Cases │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Micro Arch: Refinements¶
Какие дополнительные паттерны нужны поверх базовой архитектуры для решения конкретных проблем?
- CQS — разделение команд и запросов, берётся всегда
- CQRS — раздельные модели чтения и записи
- Domain Events in-process
- Event Sourcing — состояние как лог событий
Берутся реактивно, по факту боли.
CQRS¶
CQRS (Command Query Responsibility Segregation) - разделение модели на две: одна обрабатывает команды (меняют состояние), другая - запросы (читают состояние).
Термин ввёл Greg Young около 2010 года поверх принципа Command-Query Separation Бертрана Мейера (CQS, «Object-Oriented Software Construction», 1988). У Мейера CQS про методы: метод либо меняет состояние, либо возвращает данные, но не то и другое сразу. Young поднял идею на уровень модели целиком. Термин раскрутила статья Мартина Фаулера «CQRS» в bliki (2011).
Зачем:
- read и write нагружаются по-разному, запросов обычно на порядки больше, чем команд
- разные модели хотят разной формы данных: write - нормализованную под инварианты, read - денормализованную под конкретные экраны
- разделив пути, каждый масштабируешь и оптимизируешь независимо
команда +------------+ событие +------------+ запрос
--------------> | write | ----------------> | read model | <--------------
| model + DB | | (проекция) |
+------------+ +------------+
write-путь: инварианты, read-путь: денормализовано,
нормализация, консистентность быстро, под конкретные view
Канон и индустрия:
- Частое искажение: CQRS будто бы обязывает иметь две базы, event sourcing и async messaging. И Young, и Фаулер прямо предупреждают: это разделение на уровне модели, оно может жить в одном процессе и одной базе.
- CQRS без реальной нужды добавляет сложность, а не убирает её.
- CQRS и Event Sourcing независимы: вместе встречаются часто, но это не обязательная связка.
Когда это макро-решение:
- Разделение живёт внутри одного сервиса как способ организовать код. Это локальная деталь, в обсуждение топологии её не тащат.
- Read model - отдельное хранилище, которое обновляется событиями (EDA). Это уже решение уровня системы: хранилище, задержка обновления, eventual consistency.
Read model для запросов через границы сервисов, см. Данные и согласованность. Read model для масштабирования чтений, см. Масштабирование.