📝 Записки про тактичні патерни DDD

🎙️ Вступ

Цей текст містить нотатки, взяті з випуску "Domain Driven Design: Tactical Patterns", який був опублікований на каналі Trendyol Tech 9 травня 2024 року. Відео доступне за посиланням: Domain Driven Design: Tactical Patterns.

⚠️ Чому ці патерни?

Domain-Driven Design (DDD) є важливим посібником для правильного моделювання складних бізнес-правил. В тактичній частині DDD розглядаються патерни, які допомагають вам правильно спроектувати доменну модель.
Особливо важливо зрозуміти різницю між Anemic Model і Rich Model.

📊 Що таке Anemic Model?

  • Тільки переносить дані.
  • Бізнес-правила та валідації зберігаються окремо (зазвичай у сервісах).
  • Порушує інкапсуляцію.
  • Протирічить принципу єдиної відповідальності.
  • Бізнес-логіка просочується в шар сервісів, що ускладнює підтримку та розуміння.

✅ Що таке Rich Domain Model?

  • Дані та поведінка об’єднані разом.
  • Забезпечує інкапсуляцію.
  • Об’єкт контролює свою власну валідність.
  • Шар сервісів не потребує знань про внутрішні деталі моделі.
  • Покращена читаємість, підтримка та розширюваність.

💎 Що таке Value Object?

  • Не має ідентичності (без ID).
  • Не змінний (immutable), не змінюється після створення.
  • Самостійно перевіряє свою валідність.
  • Два value object з однаковими значеннями є рівними.
  • Приклад: валюта, відстань, номер телефону.

📦 Що таке Entity?

  • Має ідентичність (ID).
  • Відслідковується через ідентичність.
  • Може бути змінним (mutable).
  • Дві entity з однаковими значеннями, але різними ідентичностями — це різні об’єкти.

🌐 Що таке Aggregate & Aggregate Root?

  • Сукупність, яка містить кілька entity та value object.
  • Aggregate Root керує взаємодією з зовнішнім світом.
  • Прямий доступ до entity поза Aggregate Root неможливий.
  • Завантажується та зберігається атомарно (atomicity).
  • Гарантує консистентність даних.
  • Використовує версіонування (Optimistic Locking) для запобігання одночасним змінам.

⚒️ Ефективний дизайн Aggregate

  • Великі кластерні агрегати: Містять багато entity, високий ризик конфліктів.
  • Багато малих агрегатів: Надмірне дроблення ускладнює управління.
  • Оптимальні агрегати: Охоплюють правильні інваріанти, є достатньо широкими, але не надмірними.
  • Зв’язок між агрегатами повинен відбуватися лише через ID.
  • Можуть підтримуватися структурами типу Eventually Consistent.
  • Рекомендується принцип "Один агрегат на транзакцію".

📡 Domain Events

  • Повідомляють зовнішній світ про важливі події, що сталися в Aggregate.
  • Забезпечують слабку зв’язність між агрегатами.
  • Обробники подій розташовуються на рівні додатку.
  • Payload події містить лише необхідну інформацію, без додаткових даних, специфічних для споживача.
  • Використання Outbox Pattern запобігає втраті подій.

🏭 Factory

  • Відповідає за створення Aggregate, entity або value object.
  • Охоплює складні сценарії створення.
  • Може керувати інтеграцією з зовнішніми системами.
  • Розділяє логіку створення моделі та бізнес-логіку.

🛠️ Domain Service

  • Сервіси, які працюють з кількома entity або value object.
  • Не зберігають стан.
  • Зазвичай використовуються для порівняння, агрегації та обробки між entity.
  • Якщо Rich Model достатньо, потреба в domain service зменшується.
  • Необхідно уникати ризику Anemic Model.

📑 CQRS (Command Query Responsibility Segregation)

  • Command: Змінює дані (insert, update, delete).
  • Query: Зчитує дані (read).
  • Розділення запитів на читання та запис забезпечує кращу продуктивність та масштабованість.
  • Сумісність з Eventually Consistent моделями.
  • Command та Query Handler виконують одну конкретну задачу.

🔗 Event Sourcing

  • Зберігає не фінальний стан, а всі події.
  • За допомогою Projector створюється read model (останній стан) на основі подій.
  • Усі зміни можна відслідковувати.
  • Структура "Append-Only" забезпечує перевагу в продуктивності.
  • Управління Event Store, версіонування та схема еволюції викликають додаткові труднощі.

⚖️ Separation of Concerns

  • Рекомендується використовувати маленькі command та handler замість великих сервісів.
  • Легко тестується.
  • Управління помилками та транзакціями може бути поділене на події.
  • Кроки можуть бути модульно організовані за допомогою Chain of Responsibility або Pipeline Patterns.

📐 Hexagonal Architecture (Ports & Adapters)

  • Запропоновано Алістером Кокберном.
  • Мета: відокремити внутрішню частину застосунку від зовнішніх залежностей.
  • Внутрішнє ядро (Core): містить Domain та Application Logic.
  • Порти: інтерфейси, через які застосунок взаємодіє із зовнішнім світом.
  • Адаптер: Описує, як порти взаємодіють з зовнішнім світом (БД, API, UI тощо).
UI / Rest / CLI  
 |  
 Адаптер  
 |  
 Порт  
 |  
 Сервіс додатку  
 |  
 Домени
  • ✅ Незалежність тестування
  • ✅ Мінімальна залежність від технологій
  • ✅ Чіткі межі та відповідальність

🔗 Розподілені транзакції

  • У розподілених системах управління транзакціями, що охоплюють кілька сервісів і джерел даних, є складною задачею.
  • Гарантування ACID стає важким.
  • Шляхи вирішення:
  • ✅ Two-Phase Commit (2PC)
  • ✅ Saga Pattern
  • ✅ Outbox Pattern + Eventual Consistency
  • ✅ Компенсаційні транзакції (зворотні операції)

✅ Two-Phase Commit (2PC)

  • Класичний підхід до управління розподіленими транзакціями.
  • Transaction Coordinator управляє всіма учасниками.
  • Етапи:

1.
Prepare Phase: Усі учасники отримують запитання "Чи готові ви?".
2. Commit Phase: Якщо всі учасники відповідають "Готовий", виконується commit. Якщо виникає помилка, виконується rollback.

Переваги

✅ Забезпечує гарантії ACID.

Недоліки

❌ Проблеми з продуктивністю (високий час очікування).

❌ Залежність від однієї точки (Transaction Coordinator).

❌ Не рекомендується у мікросервісному світі.

🔄 Saga Pattern

  • Великий процес розділяється на ланцюг локальних транзакцій, що виконуються послідовно.
  • Кожен крок виконує свою локальну транзакцію та генерує подію.
  • Наступний сервіс, який слухає цю подію, запускає свою локальну транзакцію.

pic

Приклад процесу

  1. Служба замовлень створює замовлення (локальна транзакція).
  2. Служба замовлень відправляє подію "Отримати оплату" до служби платежів.
  3. Служба платежів отримує оплату (локальна транзакція), а потім надсилає подію до служби доставки.
  4. Служба доставки ініціює доставку (локальна транзакція).

Переваги

  • ✅ Гнучкість та масштабованість.
  • ✅ Підходить для Event-Driven систем.

Недоліки

  • ❌ Проблеми з eventual consistency.
  • ❌ Управління помилками та механізми повтору можуть бути складними.
  • ❌ Низька спостережуваність (distributed tracing) стає критичною.

pic

Порівняння

🌟 Підсумок рекомендацій

  • Для монолітних систем або операцій, що взаємодіють з одним джерелом даних: 2PC.
  • Для мікросервісів та асинхронних процесів: Saga Pattern (переважно Choreography).
  • Якщо критичний досвід користувача та необхідність у швидкому зворотному зв'язку: Outbox + Eventual Consistency.

🔥 Останні нотатки та приклади

  • Кожен domain event повинен відображати лише один стан.
  • Слід уникати event’ів, які містять дані, специфічні для споживача.
  • Domain event’и не повинні надсилатися напряму в інші bounded context (анти-патерн).
  • Domain event’и не повинні губитися, має використовуватися Outbox Pattern.
  • Транзакційні межі повинні визначатися на основі агрегатів.
  • Якщо в одній транзакції змінюється кілька агрегатів, це зазвичай є помилкою проектування.

Перекладено з: 📝 DDD Tactical Patterns Notları