Перейти к содержанию
  1. ТРЕБОВАНИЯ
  2. КЛАСС ОБЛАСТЕЙ
  3. ГРАНИЦЫ СМЫСЛА
  4. ТОПОЛОГИЯ + СВЯЗЬ + ДАННЫЕ ← один узел
  5. ПАКЕТЫ ┐
  6. ЛОГИКА │ для каждого модуля
  7. ИЗОЛЯЦИЯ │ заново
  8. НАДСТРОЙКИ ┘
  9. СКВОЗНОЕ
  10. СВОЙСТВА
  11. ПЛАТФОРМА
  12. ФИКСАЦИЯ
  13. Требования

Решаем: что вообще нужно.

Выбора нет — собираем:

Функциональные требования НФТ в числах: RPS, p99, RPO, RTO, объём данных, рост Ограничения: бюджет, срок, стек, команды, регулятор Требования к целостности: где допустима рассогласованность и на сколько

Выход: документ на 1–2 страницы с числами.

Без чего нельзя идти дальше: без чисел. «Быстро» и «надёжно» не являются требованиями.

  1. Класс областей

Решаем: сколько церемонии окупится.

Как: выписать 15–30 сценариев → сгруппировать → в каждой группе разложить 5–7 сценариев на шаги → разметить шаги как ПРАВИЛО / ПЕРЕНОС / ВЫЗОВ / РАСЧЁТ → посчитать доли.

Выбираем из:

Класс Условие Что дальше A — Data-centric правил < 15% короткий путь B — Domain-centric правил ≥ 30% полный путь C — Integration-centric вызовы внешних ≥ 30% акцент на шаге 8 D — Compute-centric ценность = расчёт акцент на алгоритме E — Workflow-centric мало правил, много ожиданий во времени оркестратор

Выход: метка на каждую область.

Правило: классов должно быть больше одного. Если вся система получила одну букву — перепроверьте.

  1. Границы смысла

Решаем: из каких предметных областей состоит система.

Делаем только для областей класса 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.

  1. Топология + связь + данные

Решаем: сколько исполняемых единиц. Три вопроса решаются вместе, не последовательно.

Выбираем из — топология:

Вариант Когда Монолит прототип, малая команда Модульный монолит дефолт Микросервисы есть независимая причина

Независимые причины разделения — нужна хотя бы одна:

разный профиль нагрузки разные команды с разным циклом релиза разные требования к доступности разный стек регуляторная изоляция данных

Ни одной — модульный монолит.

Выбираем из — коммуникация: REST · gRPC · GraphQL · messaging · WebSockets

Выбираем из — данные: RDBMS · NoSQL · TimeSeries · Graph · polyglot · sharding · replication · caching · event store

Правило владения: одна таблица — один владелец. Шаринг БД между контекстами убивает границы.

Выход: число deployable units, схема связи, карта хранилищ.

  1. Пакеты

Шаги 4–7 проходятся заново для каждого модуля.

Решаем: чем определён верхний уровень пакетов.

Выбираем из:

Вариант Когда By layer класс A, маленький проект By feature дефолт Modular Monolith (api/ + internal/) модулей > 3 или несколько команд Vertical Slice сценарии сильно разной сложности

Критерий проверки — Screaming: верхний уровень читается как order, billing, shipping, не как controller, service, repository.

Обязательно: ArchUnit или Spring Modulith. Границы без автопроверки исчезают за два спринта.

  1. Логика

Решаем: где физически живёт логика.

Выбираем из (тройка Фаулера):

Вариант Когда 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 вместо примитивов.

  1. Изоляция

Решаем: как защитить ядро. Определяется тем, что вышло на шаге 5.

Выбираем из:

Вариант Когда Без изоляции класс A, прототип Layered класс A/C, Transaction Script Hexagonal / Onion / Clean класс B, Rich Domain Model Functional Core логика как преобразование

Выбор между Hexagonal, Onion и Clean — вопрос словаря команды, не архитектуры. Одно семейство.

Проверка: бизнес-правило тестируется без Spring-контекста и без базы. Если нужен @SpringBootTest — изоляции нет.

  1. Надстройки

Решаем: какие дополнительные разделения. Берутся реактивно, по факту боли.

Надстройка Триггер CQS всегда — бесплатно CQRS ур. 2 (QueryService с прямым SQL) модель чтения разошлась с моделью записи Domain Events in-process модули слиплись Event Sourcing бизнесу нужна история, не состояние

Для событий — выбрать гарантию:

Режим Когда Синхронный в транзакции строгая согласованность AFTER_COMMIT неважные побочные эффекты Outbox дефолт для важных

  1. Сквозное

Решаем один раз на систему:

Транзакции: @Transactional только в Application Service; транзакция = агрегат Ошибки: домен → 422, сценарий → 404, техническая → 503; домен не знает про HTTP Валидация: формат → адаптер, значение → VO, инвариант → агрегат Тесты: доля тестов, требующих Spring — показатель качества изоляции Идемпотентность, retry, DLQ — обязательно для класса C

  1. Свойства

Решаем под уже выбранную структуру:

Resilience (таймауты, retry, circuit breaker, bulkhead) · Observability (метрики, логи, трассировки, алерты) · Security-механизмы · Build & Delivery · Compliance

Требования к consistency, нагрузке и безопасности пришли на шаге 0.

  1. Платформа

Bare-metal · VM · контейнеры · K8s/Nomad · Cloud PaaS

Если платформа задана извне — это было ограничение шага 0, а не решение здесь.

  1. Фиксация 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

Что является основной единицей организации кода?

  1. Package by Layer Техническая роль: controller / service / repository / model

  2. Package by Feature Бизнес-возможность / предметная область: order / payment / catalog

  3. 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
  1. Transaction Script Идея максимально простая: Каждая бизнес-операция = процедура/метод, который выполняет весь сценарий от начала до конца.

  2. Table Module Здесь идея другая: Для каждой таблицы/основной структуры данных существует объект, содержащий бизнес-логику для всех записей этой таблицы.

  3. 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 для масштабирования чтений, см. Масштабирование.