DDD

Глава 9. Патерни Взаємодії

У попередніх розділах (глави 5–8) ми фокусувалися на тактичних патернах — як будувати окремі компоненти системи, моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми навчилися створювати Агрегати, Об'єкти-Значення та організовувати їх у Шарувату або Гексагональну архітектуру.

Глава 9. Патерни Взаємодії

У попередніх розділах (глави 5–8) ми фокусувалися на тактичних патернах — як будувати окремі компоненти системи, моделювати бізнес-логіку всередині одного Обмеженого Контексту (Bounded Context). Ми навчилися створювати Агрегати, Об'єкти-Значення та організовувати їх у Шарувату або Гексагональну архітектуру.

Але жоден сервіс не існує у вакуумі.

У цій главі ми вийдемо за межі одного компонента і розглянемо, як організувати "спілкування" між різними частинами системи. Ми поговоримо про те, як дані перетікають між контекстами, як забезпечити узгодженість (consistency) у розподілених системах і як керувати складними бізнес-процесами, що охоплюють кілька сервісів.

Трансляція Моделей

Як "подружити" різні мови та моделі даних різних контекстів без створення хаосу.

Інтеграція Агрегатів

Як публікувати події з агрегатів надійно, не втрачаючи даних при збоях.

Сага та Менеджер Процесів

Як керувати довготривалими бізнес-процесами, що охоплюють транзакції в кількох сервісах.

1. Трансляція Моделей (Model Translation)

Обмежений Контекст — це кордон, всередині якого існує певна Єдина Мова (Ubiquitous Language). Термін "Користувач" у контексті Продажів може означати "Покупець", а у контексті Доставки — "Отримувач". У них різні атрибути, різна поведінка і різний зміст.

Коли два контексти взаємодіють, їм потрібно "перекладати" терміни з однієї мови на іншу. Без явного перекладу ми ризикуємо "забруднити" нашу чисту модель чужими концепціями.

Патерни Інтеграції (нагадування)

Згадаємо стратегічні патерни з Глави 3:

  1. Partnership (Партнерство): Команди синхронізують зміни. Переклад може бути мінімальним.
  2. Shared Kernel (Спільне Ядро): Частина моделі є спільною (наприклад, бібліотека з DTO). Переклад не потрібен для спільної частини.
  3. Customer-Supplier (Клієнт-Постачальник): Постачальник може диктувати формат, або Клієнт змушений адаптуватися.
  4. Conformist (Конформіст): Клієнт повністю приймає модель Постачальника "як є", без перекладу. (Часто це погана ідея).
  5. Anticorruption Layer (ACL): Клієнт створює захисний шар, який транслює зовнішню модель у свою внутрішню.
  6. Open-Host Service (OHS): Постачальник створює публічний API (Published Language), який відрізняється від його внутрішньої моделі, щоб захистити клієнтів від змін.

У цій главі ми зосередимося на технічній реалізації ACL та OHS.

Stateless Translation (Переклад без стану)

Це найпростіший вид трансляції. Він відбувається "на льоту" (on-the-fly) під час проходження запиту або повідомлення.

Синхронна взаємодія (Proxy)

Уявіть, що ваш сервіс замовлень (OrderContext) повинен отримати інформацію про клієнта з CRM (CustomerContext).

Замість того, щоб розкидувати виклики до CRM по всьому коду, ми створюємо Proxy (або Адаптер).

Loading diagram...

sequenceDiagram participant Domain as Order Domain participant ACL as Anti-Corruption Layer participant CRM as External CRM

Domain->>ACL: GetCustomer(id)
Note right of Domain: Використовує внутрішню модель (CustomerEntity)

ACL->>CRM: GET /api/v1/partners/{id}
CRM-->>ACL: JSON { partner_id, name, address... }
Note right of CRM: Зовнішня модель (PartnerDTO)

ACL->>ACL: Map PartnerDTO -> CustomerEntity
ACL-->>Domain: CustomerEntity

Де це живе в коді? В архітектурі Портів та Адаптерів:

  • Порт: ICustomerGateway (визначений в Domain Layer).
  • Адаптер: CrmCustomerAdapter (реалізований в Infrastructure Layer).
// Domain Layer
public record Customer(CustomerId Id, string FullName, Address ShippingAddress);

public interface ICustomerGateway
{
    Task<Customer?> GetCustomerAsync(CustomerId id);
}

// Infrastructure Layer (ACL)
public class CrmResponse
{
    public string PartnerId { get; set; }
    public string PartnerName { get; set; } // В CRM це називається PartnerName
    public string Street { get; set; }
    public string City { get; set; }
}

public class HttpCrmAdapter : ICustomerGateway
{
    private readonly HttpClient _client;

    public async Task<Customer?> GetCustomerAsync(CustomerId id)
    {
        var response = await _client.GetFromJsonAsync<CrmResponse>($"/partners/{id}");

        // Ось тут відбувається магія трансляції (Mapping)
        return new Customer(
            new CustomerId(response.PartnerId),
            response.PartnerName,
            new Address(response.Street, response.City)
        );
    }
}
ПорадаНе використовуйте AutoMapper для ACL. Логіка трансляції часто складніша, ніж просте співставлення полів 1-в-1. Пишіть мапери вручну — це робить код явним і стійким до рефакторингу.

Асинхронна взаємодія (Message Translator)

Коли системи спілкуються через повідомлення (RabbitMQ, Kafka), трансляція також необхідна. Якщо BillingContext підписаний на події OrderContext, він не повинен залежати від внутрішньої структури подій OrderContext.

  1. Public Events: OrderContext публікує спеціальні "інтенціональні" події (Integration Events), які є стабільним контрактом (Published Language).
  2. Consumer Translator: Якщо OrderContext публікує "брудні" події, BillingContext може мати вхідний адаптер, який конвертує їх у внутрішні команди перед обробкою.
Loading diagram...

graph LR subgraph Order Context DoDomain Logic -->|Internal Event| PubPublisher Pub -->|Translation| OHSOpen Host Service end

OHS -->|Integration Event| Bus((Message Bus))

subgraph Billing Context
    Bus -->|Integration Event| ACL[Anti-Corruption Layer]
    ACL -->|Translation| Cmd[Internal Command]
    Cmd --> Bil[Billing Logic]
end

style Bus fill:#f9f,stroke:#333
style OHS fill:#b3e5fc
style ACL fill:#ffccbc

Stateful Translation (Переклад зі станом)

Іноді простого мапінгу недостатньо. Вам може знадобитися:

  • Агрегація: Зібрати дані з трьох різних сервісів, щоб сформувати одну внутрішню сутність.
  • Де-дуплікація: Відфільтрувати повторювані повідомлення.
  • Збагачення (Enrichment): Отримати ID, а потім зробити запит, щоб отримати повні дані.

Для цього потрібне збереження стану в БД.

Aggregator Pattern

Уявіть, що ви будуєте Backend for Frontend (BFF). Мобільний додаток хоче отримати "Деталі Замовлення одним запитом". А у вас мікросервіси:

  1. Сервіс Замовлень (основна інфа).
  2. Сервіс Доставки (статус трекінгу).
  3. Сервіс Оплати (статус транзакції).
  4. Сервіс Каталогу (картинки товарів).

Ваш BFF виступає як Stateful Translator (або просто Aggregator).

Loading diagram...

graph TD ClientMobile App -->|GET /orders/123| BFFBFF Aggregator

BFF -->|Parallel Call| S1[Order Service]
BFF -->|Parallel Call| S2[Delivery Service]
BFF -->|Parallel Call| S3[Payment Service]
BFF -->|Parallel Call| S4[Catalog Service]

S1 --> BFF
S2 --> BFF
S3 --> BFF
S4 --> BFF

BFF -->|Unified JSON| Client

Якщо це відбувається асинхронно (через події), вам знадобиться локальна база даних у сервісі-агрегаторі, щоб зберігати проміжні результати ("прийшла інфа від доставки, чекаємо на інфу від оплати"). Це вже нагадує патерн Process Manager, про який ми поговоримо пізніше.


Інтеграція з Legacy Системами (Bubble Context)

Одна з найскладніших задач — це інтеграція нового чистого DDD-рішення з великою "кулею бруду" (Big Ball of Mud) або застарілою системою (Legacy).

Тут виникає дилема:

  • Якщо ми будемо використовувати модель даних Legacy-системи, ми заразимо наш новий контекст поганими абстракціями.
  • Якщо ми будемо будувати ідеальну модель, нам доведеться писати дуже складний шар трансляції.

Bubble Context (Контекст-Бульбашка)

Це стратегія захисту нового коду. Ми створюємо "бульбашку", всередині якої діє наша чиста модель. На кордонах цієї бульбашки стоїть дуже товстий ACL.

Стратегії ACL для Legacy

  1. Synchronizing ACL: Legacy база даних є "майстром". Наш ACL періодично вичитує дані з Legacy БД і оновлює нашу чисту базу даних.
  2. Event-Intercepting ACL: Ми ставимо тригери на Legacy БД або перехоплюємо зміни (CDC - Change Data Capture), щоб генерувати події для нашої системи.
  3. Strangler Fig (Фіга-Душитель): Ми поступово замінюємо частини Legacy, перенаправляючи трафік на новий контекст.
Loading diagram...

graph TD User-->|Request| Router Router-->|Route /new-api| NewNew DDD Context Router-->|Route /old-api| LegacyLegacy Monolith

New-->|Anti-Corruption Layer| Legacy
Legacy-.->|Database Replication| New

Change Data Capture (CDC)

Іноді Legacy система не вміє публікувати події. Вона просто пише в базу. Щоб інтегруватися, ми можемо використати патерн CDC (наприклад, Debezium + Kafka).

  1. Legacy пише в MySQL.
  2. Debezium читає binlog MySQL.
  3. Debezium публікує "сирі" події (db.table.row_changed) в Kafka.
  4. Наш ACL слухає ці події, транслює їх у Domain Events (OrderConfirmed) і відправляє в наш Context.

Це дозволяє перетворити "німий" моноліт на джерело подій.


2. Інтеграція Агрегатів (Transactional Integrity)

Головне правило Агрегатів: Одна транзакція — Один Агрегат. Ми не змінюємо Order і Customer в одній транзакції бази даних. Це порушує межі контекстів і створює проблеми з блокуваннями.

Але бізнес-процеси часто зачіпають кілька агрегатів. Як бути? Відповідь: Eventual Consistency (Узгодженість у кінцевому рахунку).

  1. Агрегат А змінюється і публікує подію DomainEvent.
  2. Агрегат Б слухає цю подію і змінюється у відповідь.

Головна проблема тут — надійність публікації подій.

Проблема "Dual Write" (Подвійний запис)

Розглянемо наївний (і неправильний) код:

public async Task DeactivateCampaign(Guid id)
{
    // 1. Зміна стану в БД
    var campaign = await _repo.GetAsync(id);
    campaign.Deactivate();
    await _db.SaveChangesAsync(); // Transaction 1

    // 2. Публікація в чергу (RabbitMQ/Kafka)
    var evt = new CampaignDeactivated(id);
    await _bus.PublishAsync(evt); // Transaction 2 (?)
}

Що може піти не так?

  1. SaveChangesAsync пройшов, а _bus.PublishAsync впав (мережа, таймаут, брокер лежить).
    • Результат: Кампанія деактивована, але ніхто про це не знає. Система неузгоджена.
  2. (Якщо поміняти місцями) _bus.PublishAsync пройшов, а SaveChangesAsync впав.
    • Результат: Подія пішла ("Кампанія деактивована"), а в базі вона досі активна. Система бреше.

Ви не можете просто так обгорнути базу і RabbitMQ в одну розподілену транзакцію (2PC - Two Phase Commit). Це повільно, складно і часто не підтримується хмарними брокерами.

Рішення: Outbox Pattern.

Outbox Pattern (Патерн Вихідної Скриньки)

Ідея геніальна у своїй простоті: використовуйте локальну транзакцію БД для обох дій.

Замість того, щоб слати подію в брокер одразу, ми записуємо її в спеціальну таблицю OutboxMessages в тій самій базі даних, де лежить наш Агрегат.

Чому це важливо?

Уявіть фінансову систему. Якщо подія MoneyTransferred загубиться, гроші просто зникнуть. Outbox гарантує, що подія буде збережена в тій же транзакції, що і зміна балансу.

Деталі реалізації Outbox

Таблиця OutboxMessages повинна мати такі колонки:

  • Id (UUID): Унікальний ідентифікатор повідомлення.
  • Type (string): Тип події (повне ім'я класу).
  • Payload (json/blob): Самі дані події.
  • OccurredOn (timestamp): Коли це сталося.
  • TraceId (string): Для розподіленого трейсингу (OpenTelemetry).
  • CorrelationId (string): ID бізнес-транзакції або Causing Event ID.

Relay (Poller vs Transaction Log Tailing) Є два способи дістати дані з Outbox таблиці:

  1. Polling Publisher: Простий JOB, який робить SELECT * FROM Outbox WHERE Processed = 0 кожні X секунд.
    • Плюси: Легко реалізувати. Працює з будь-якою SQL БД.
    • Мінуси: Затримка (Latency). Навантаження на БД. Можливі конфлікти (Race conditions) при багатьох інстансах.
  2. Transaction Log Tailing (CDC): Використання Debezium для читання змін саме в таблиці Outbox.
    • Плюси: Майже нульова затримка. Не вантажить БД запитами.
    • Мінуси: Складніша інфраструктура (Kafka, Debezium Connectors).
ПорадаПочинайте з Polling Publisher. Це покриває 90% кейсів. Переходьте на CDC тільки якщо у вас High Load.

Алгоритм

  1. Почати транзакцію БД.
  2. Зберегти зміни Агрегата (напр. таблиця Campaigns).
  3. Зберегти подію в таблицю OutboxMessages (insert JSON).
  4. Закомітити транзакцію.
    • Атомарність гарантує ACID вашої БД (Postgres/MySQL/MSSQL). Або збережеться все, або нічого.
  5. Окремий фоновий процес (Relay) читає з таблиці Outbox і пересилає в RabbitMQ.
  6. Після успішної відправки — видаляє запис з Outbox або позначає як Processed.
Loading diagram...

sequenceDiagram participant App as Application participant DB as Database (Transaction) participant Relay as Background Worker participant Bus as Message Bus

App->>DB: BEGIN TRANSACTION
App->>DB: UPDATE Campaigns SET Active=0...
App->>DB: INSERT INTO Outbox (Type, Payload) VALUES ('CampaignDeactivated', '{...}')
App->>DB: COMMIT TRANSACTION

Note right of App: Дані узгоджені гарантовано

loop Every 1 sec
    Relay->>DB: SELECT * FROM Outbox WHERE Processed = 0
    DB-->>Relay: [Event1, Event2]

    Relay->>Bus: Publish(Event1)
    Bus-->>Relay: ACK

    Relay->>DB: UPDATE Outbox SET Processed = 1 WHERE Id = Event1
end

Реалізація на C# (EF Core)

// 1. Сутність Outbox Message
public class OutboxMessage
{
    public Guid Id { get; set; }
    public string Type { get; set; }
    public string Data { get; set; } // JSON
    public DateTime OccurredOn { get; set; }
    public DateTime? ProcessedDate { get; set; }
}

// 2. Збереження в одній транзакції
public async Task DeactivateCampaign(Guid id)
{
    using var transaction = _context.Database.BeginTransaction();

    var campaign = await _context.Campaigns.FindAsync(id);
    campaign.Deactivate(); // Генерує DomainEvents в пам'яті

    // Перетворюємо Domain Events на Outbox Messages
    var domainEvents = campaign.DomainEvents;
    foreach (var evt in domainEvents)
    {
        var outboxMsg = new OutboxMessage
        {
            Id = Guid.NewGuid(),
            Type = evt.GetType().Name,
            Data = JsonSerializer.Serialize(evt, evt.GetType()),
            OccurredOn = DateTime.UtcNow
        };
        _context.OutboxMessages.Add(outboxMsg);
    }

    await _context.SaveChangesAsync();
    await transaction.CommitAsync();
}
At-Least-Once DeliveryOutbox гарантує, що повідомлення буде відправлено хоча б один раз. Але воно може бути відправлено більше одного разу. Якщо Relay відправив повідомлення в RabbitMQ, але впав до того, як оновив статус в БД (UPDATE Outbox), він відправить його знову після перезапуску. Тому ваші консюмери (послухачі) повинні бути Ідемпотентними (Idempotent). Тобто, повторна обробка тієї ж події не повинна ламати систему.

3. Сага (Saga)

Якщо Outbox вирішує проблему атомарності всередині одного сервісу при публікації, то Сага керує бізнес-процесом, що охоплює кілька сервісів.

Сага — це послідовність локальних транзакцій. Кожна транзакція оновлює дані в одному сервісі і публікує подію, що тригерить наступний крок. Якщо щось йде не так, Сага запускає Компенсуючі транзакції (Compensating Transactions), щоб відкотити зміни.

У реляційних БД ми звикли до ACID. У мікросервісах ми маємо BASE (Basically Available, Soft state, Eventual consistency).

Приклад: Бронювання Подорожі

Процес:

  1. Забронювати готель (HotelService).
  2. Забронювати авто (CarService).
  3. Забронювати літак (FlightService).

Якщо літак не вдалося забронювати (місць немає), ми повинні скасувати бронь авто і готелю.

Ми не можемо просто ROLLBACK в розподіленій системі. Ми повинні виконати команду CancelCarBooking та CancelHotelBooking.

Типи Саг

1. Хореографія (Choreography)

Сервіси спілкуються подіями децентралізовано. Немає єдиного "диригента".

  1. Order Service: OrderCreated -> публікує подію.
  2. Hotel Service: слухає OrderCreated -> бронює готель -> публікує HotelBooked.
  3. Car Service: слухає HotelBooked -> бронює авто -> публікує CarBooked.
  4. Flight Service: слухає CarBooked -> провал -> публікує FlightBookingFailed.
  5. Car Service: слухає FlightBookingFailed -> скасовує авто.
  6. Hotel Service: слухає FlightBookingFailed -> скасовує готель.
  7. Order Service: слухає FlightBookingFailed -> позначає замовлення як "Failed".
  • Переваги: Просто почати, мала зв'язність (loose coupling), немає єдиної точки відмови.
  • Недоліки: Важко зрозуміти весь процес (він розмазаний по всіх сервісах). "Пекло подій" (Event Hell). Циклічні залежності.

2. Оркестрація (Orchestration)

Є центральний координатор (Оркестратор), який каже всім, що робити.

  1. Orchestrator: Посилає команду BookHotel (RPC або Command Message).
  2. Hotel Service: HotelBooked.
  3. Orchestrator: Ок, далі. Посилає команду BookCar.
  4. Car Service: CarBooked.
  5. Orchestrator: Ок, далі. Посилає команду BookFlight.
  6. Flight Service: FlightFailed.
  7. Orchestrator: Ой! Викликає CancelCar і CancelHotel.
  • Переваги: Процес явний і записаний в одному місці. Легко керувати помилками і таймаутами.
  • Недоліки: Оркестратор стає "розумним", а сервіси "тупими" (CRUD). Центральна точка відмови (хоча це лікується реплікацією).
Для складних процесів (більше 2-3 кроків) використовуйте Оркестрацію. Хореографія підходить тільки для дуже простих, лінійних процесів.

4. Process Manager (Менеджер Процесів)

Менеджер Процесів — це еволюція Оркестратора. Якщо Сага — це просто набір кроків "вперед-назад" для досягнення узгодженості, то Менеджер Процесів — це State Machine (машина станів), яка керує довготривалим бізнес-процесом.

Він має свій власний стан (State), який зберігається в БД.

Анатомія Process Manager

Це Агрегат. Так, звичайний DDD Агрегат, але його призначення — не зберігати бізнес-дані (як профіль користувача), а зберігати стан процесу.

public class TripBookingSaga : AggregateRoot
{
    private TripId _id;
    private BookingState _state; // New, HotelBooked, CarBooked, Confirmed, Failed
    private Dictionary<string, string> _bookingIds; // Зберігаємо ID зовнішніх броней

    public void Handle(HotelBooked evt)
    {
        if (_state != BookingState.New) return; // Ідемпотентність

        _bookingIds["Hotel"] = evt.HotelBookingId;
        _state = BookingState.HotelBooked;

        // Вирішуємо, що робити далі
        AddDomainEvent(new BookCarCommand(...));
    }

    public void Handle(CarBooked evt)
    {
         if (_state != BookingState.HotelBooked) return;

         _bookingIds["Car"] = evt.CarBookingId;
         _state = BookingState.CarBooked;

         AddDomainEvent(new BookFlightCommand(...));
    }

    public void Handle(FlightBookingFailed evt)
    {
        _state = BookingState.Cancelling;

        // Запускаємо компенсацію
        AddDomainEvent(new CancelCarCommand(_bookingIds["Car"]));
        AddDomainEvent(new CancelHotelCommand(_bookingIds["Hotel"]));
    }
}
Loading diagram...

stateDiagram-v2 * --> New New --> HotelBooked: Hotel Confirmed HotelBooked --> CarBooked: Car Confirmed CarBooked --> Confirmed: Flight Confirmed Confirmed --> *

CarBooked --> Cancelling: Flight Failed
HotelBooked --> Cancelling: Car Failed
Cancelling --> Failed: All Cancelled
Failed --> [*]

Відмінність Саги від PM

Часто ці терміни вживають як синоніми. Але технічно:

  • Сага: Патерн управління транзакціями (як відкотити, якщо впало).
  • Process Manager: Більш загальний патерн. Він може чекати на подію "Користувач підтвердив email" 3 дні. Він може мати складну логіку if-else, таймери ("якщо водій не знайшовся за 5 хв, скасувати замовлення").

5. Еволюція Контрактів Подій (Schema Evolution)

Одна з найбільших проблем розподілених систем — це зміни. Ви змінили структуру події OrderCreated (перейменували поле), і раптом 5 інших сервісів впали з помилкою десеріалізації.

Подія — це публічний API. Ви не можете змінювати його як заманеться.

Стратегії Версійності

1. Адитивні зміни (Additive Changes)

Найбезпечніша стратегія. Ви тільки додаєте нові поля, але ніколи не видаляєте і не перейменовуєте старі. JsonSerializer зазвичай ігнорує незнайомі поля, тому старі консюмери просто не побачать нових даних, але продовжать працювати.

2. Явна Версійність (Explicit Versioning)

Включайте версію в структуру події або в заголовок повідомлення.

{
  "eventId": "...",
  "eventType": "OrderCreated.v2",
  "data": { ... }
}

Або в класі C#:

public class OrderCreatedV1 { ... }
public class OrderCreatedV2 { ... }

Ваш ACL (Message Translator) може приймати обидві версії і мапити їх на одну внутрішню команду.

3. Weak Schema (Слабка Схема)

Консюмер не повинен десеріалізувати всю подію. Він повинен витягувати тільки ті поля, які йому потрібні. Замість типізованого DTO (OrderCreatedDto), використовуйте JsonDocument або динамічний доступ, якщо це дозволяє мова.

Це патерн Tolerant Reader (Толерантний Читач).

Schema Registry

Для серйозних систем (особливо з Kafka) використовуйте Schema Registry (наприклад, Confluent Schema Registry). Це сервіс, який зберігає всі версії ваших контрактів (Avro, Protobuf, JSON Schema). Продюсер перед відправкою валідує повідомлення зі схемою. Якщо ви спробуєте відправити несумісну зміну, Schema Registry відхилить її на етапі CI/CD або Runtime.


6. Тестування Взаємодії

Як переконатися, що Сага працює, не піднімаючи 10 докер-контейнерів?

Unit Tests

Тестуйте Агрегати і Саги ізольовано. Сага (Process Manager) — це просто клас, який приймає DomainEvent і повертає Command (або список команд). Це чиста функція (майже).

[Fact]
public void Should_book_car_after_hotel_booked()
{
    // Arrange
    var saga = new TripBookingSaga();
    saga.TransitionTo(BookingState.New);

    // Act
    saga.Handle(new HotelBooked { HotelBookingId = "h-123" });

    // Assert
    saga.State.Should().Be(BookingState.HotelBooked);
    saga.DomainEvents.Should().Contain(e => e is BookCarCommand);
}

Integration Tests (Outbox)

Перевірте, що збереження в БД і запис в Outbox відбуваються атомарно. Використовуйте TestContainers з реальною БД.

Contract Tests (Consumer-Driven Contracts)

Це "золотий стандарт" для мікросервісів. Замість того, щоб Продюсер гадав, що потрібно Консюмеру, Консюмер пише контракт-тест ("Я очікую, що в JSON прийде поле userId типу String").

Ці контракти ("Pacts") публікуються на білдервер (Pact Broker). Продюсер на своєму CI проганяє ці контракти. Якщо він видалив поле userId, білд впаде, і він не зможе задеплоїтися.

Інструмент: Pact.NET.


7. Часті Питання (FAQ)


5. Підсумки та Рекомендації

Інтеграція — це місце, де система стає крихкою. Використовуйте ці патерни, щоб повернути контроль.

  1. Завжди захищайте свої Агрегати. Ніколи не публікуйте події без Outbox або іншого механізму гарантованої доставки, якщо вам важлива узгодженість.
  2. Не бійтеся дублювання даних. Краще мати копію імені клієнта в сервісі замовлень (очевидно через ACL), ніж робити синхронний HTTP запит до CRM кожного разу, коли треба показати список замовлень.
  3. Use ID references. Між контекстами передавайте тільки ID. CustomerId, ProductId. Не передавайте цілі об'єкти.
  4. Асинхронність — друг. Синхронні виклики (HTTP/gRPC) створюють часовий зв'язок (temporal coupling). Якщо сервіс Б лежить, сервіс А теж лежить. Черги повідомлень розривають цей зв'язок.
ПатернКоли використовуватиСкладність
ACL (Proxy)Синхронний виклик або простий мапінг подій. Захист від легасі.Низька
OutboxПублікація подій з гарантією доставки. Must-have для DDD.Середня
Choreography SagaПрості процеси (2-3 кроки).Середня
Orchestration SagaСкладні процеси, багато кроків, компенсації.Висока
Process ManagerДовготривалі процеси, таймери, очікування людської реакції.Висока

У наступній главі ми поговоримо про Евристики Проектування — як вибирати правильні інструменти та патерни, дивлячись на код і вимоги бізнесу, а не сліпо слідуючи книжкам.

Copyright © 2026