Ddd

Інтеграція обмежених контекстів

Паттерни інтеграції Обмежених контекстів, типи командної співпраці та стратегічні рішення в Domain-Driven Design

Інтеграція обмежених контекстів (Bounded Context Integration)

Ключова ідея главиОбмежені контексти не є ізольованими островками. Вони повинні взаємодіяти один з одним для формування цілісної системи. Паттерни інтеграції допомагають визначити, як саме ці контексти співпрацюють, з урахуванням організаційної структури та балансу сил між командами.

Вступ

У попередній главі ми дізнались, що Обмежені контексти (Bounded Contexts) захищають узгодженість Єдиної мови всередині своїх меж і відкривають можливості до побудови моделей. Ми розуміємо, що:

  • Модель не може існувати без меж
  • Єдина мова діє лише всередині контексту
  • Різні контексти можуть мати різні моделі одних і тих самих бізнес-сутностей
Важлива істинаХоча моделі всередині Обмежених контекстів розвиваються незалежно, самі контексти НЕ є незалежними. Система не може складатися з повністю ізольованих компонентів — вони мають взаємодіяти для досягнення бізнес-цілей.

Проблема інтеграції

Коли два Обмежені контексти потребують взаємодії, виникають фундаментальні питання:

Що ми вивчимо?

У цій главі розглянемо паттерни інтеграції DDD, які визначаються характером співпраці між командами:

Крок 1: Співпраця (Cooperation)

Паттерни для команд з щільною взаємодією: Partnership та Shared Kernel.

Крок 2: Споживач-Постачальник (Customer-Supplier)

Паттерни з явним балансом сил: Conformist, ACL, OHS.

Крок 3: Різні шляхи (Separate Ways)

Коли команди вирішують НЕ інтегруватися.

Крок 4: Карта контекстів

Візуалізація інтеграцій і командних зв'язків.


Співпраця (Cooperation Patterns)

Паттерни співпраці стосуються Обмежених контекстів, над якими працюють команди з добре налагодженою взаємодією.

Визначення: СпівпрацяCooperation — тип відносин між ограниченими контекстами, за якого команди мають взаємозалежні цілі. Успіх однієї команди залежить від успіху іншої, і навпаки.

Характеристики співпраці

АспектОпис
СпілкуванняЩ density, часта синхронізація
ЦіліВзаємозалежні, спільний успіх
ВідповідальністьСпільна за інтеграцію
КонфліктиВирішуються через обговорення
ГнучкістьВисока, обидві сторони адаптуються
Коли застосовувати
  • Обидва контексти належать одній команді
  • Команди працюють в одному офісі
  • Цілі бізнес-команд тісно пов'язані
  • Є можливість для частих зустрічей

Партнерство (Partnership Pattern)

Partnership — найпростіший паттерн співпраці. Інтеграція координується ситуативно, "на льоту".

Як працює партнерство

Loading diagram...
graph LR
    A[Контекст A] <-->|Двостороння координація| B[Контекст B]

    A -->|API зміни| B
    B -->|Адаптація| A

    style A fill:#e3f2fd
    style B# c8e6c9
Ключові характеристикиДвостороння координація: Жодна команда не нав'язує свою мову
Гнучкість: Зміни обговорюються та узґоджуються
Без конфліктів: Команди співпрацюють без драм
Синхронізація: Часта і безперервна

Приклад: Управління замовленнями та доставкою

Уявімо інтернет-магазин з двома контекстами:

Контекст Замовлень

Контекст Доставки

Сценарій інтеграції:

Крок 1: Зміна в контексті Замовлень

Команда Замовлень вирішує додати нове поле preferredDeliveryTime до моделі Order.

Крок 2: Уведомлення команди Доставки

Команда Замовлень повідомляє: "Ми додаємо час бажаної доставки. Вам це потрібно?"

Крок 3: Обговорення

Обидві команди обговорюють:

  • Чи може Доставка використати цю інформацію?
  • Як буде виглядати API контракт?
  • Хто і коли внесе зміни?

Крок 4: Узгоджена імплеметація

Обидві команди адаптують свої моделі разом, у дусі партнерства.

Технічна реалізація

// Контекст Замовлень
interface OrderCreatedEvent {
    orderId: string
    customerId: string
    items: OrderItem[]
    deliveryAddress: Address
    preferredDeliveryTime?: Date // Нове поле
}

// Публікація події
eventBus.publish('order.created', orderCreatedEvent)
// Контекст Доставки
eventBus.subscribe('order.created', (event: OrderCreatedEvent) => {
  const shipment = новийПоле Shipment({
    orderId: event.orderId,
    address: event.deliveryAddress,
    preferredTime: event.preferredDeliveryTime, // Використання нового поля
  });

  shipment Booking.schedule();
});

Переваги партнерства

Швидка адаптація

Спільна відповідальність

Узгодженість

Недоліки та обмеження

Коли партнерство НЕ працюєГеографічна віддаленість: Різні часові пояси ускладнюють синхронізацію
Різні пріоритети: Команди конкурують за ресурси
Великі команди: Координація стає складною
Відсутність довіри: Команди не готові співпрацювати

Практичні поради

Як зробити партнерство успішним
  1. Непрерывна інтеграція (CI): Автоматизуйте тести інтеграції
  2. Короткі цикли: Синхронізуйтесь щодня або щотижня
  3. Спільні зустрічі: Регулярні стендапи або планування
  4. Документація: Фіксуйте домовленості про контракти
  5. Повага до змін: Не нав'язуйте зміни без обговорення

Спільне ядро (Shared Kernel Pattern)

Shared Kernel — паттерн, за якого кілька Обмежених контекстів розділяють спільну модель або її частину.

Увага: Винято з правил!Цей паттерн порушує принцип, що кожен Обмежений контекст має свою модель. Використовуйте його обережно і лише за необхідності!

Концепція Спільного ядра

Loading diagram...
graph TB
    subgraph BC1["Контекст A"]
        A1[Модель A]
    end

    subgraph SK["Спільне Ядро"]
        S[Shared Model]
    end

    subgraph BC2["Контекст B"]
        B1[Модель B]
    end

    A1 -.Використовує.-> S
    B1 -.Використовує.-> S

    style SK fill:#ffcdd2
    style BC1 fill:#e3f2fd
    style BC2 fill:#c8e6c9

Приклад: Корпоративна система авторизації

Розглянемо систему з власною моделлю управління правами доступу (Authorization Model).

Проблема:
Кілька конт екстів потребують:

  • Перевірки прав користувача
  • Управління ролями
  • Аудиту доступу

Рішення: Спільне ядро AuthorizationKernel

// Спільне ядро (Shared Kernel)
namespace Company.AuthorizationKernel
{
    public class User
    {
        public UserId Id { get; private set; }
        public List<Role> Roles { get; private set; }
        public List<Permission> DirectPermissions { get; private set; }

        public bool HasPermission(Permission permission)
        {
            // Логіка перевірки: прямі дозволу + успадковані від ролей
            return DirectPermissions.Contains(permission) ||
                   Roles.A ny(r => r.Permissions.Contains(permission));
        }
    }

    public class Role
    {
        public RoleId Id { get; private set; }
        public string Name { get; private set; }
        public List<Permission> Permissions { get; private set; }
    }

    public record Permission(string Resource, string Action);
}

Використання в різних контекстах:

//Контекст Sales
namespace Company.SalesContext
{
    using Company.AuthorizationKernel;

    public class SalesOrderService
    {
        public void CreateOrder(OrderData data, User user)
        {
            // Використання Спільного ядра
            if (!user.HasPermission(new Permission("Orders", "Create")))
                throw new UnauthorizedException();

            //Основни logic...
        }
    }
}

// Контекст Inventory
namespace Company.InventoryContext
{
    using Company.AuthorizationKernel;

    public class WhaleouseService
    {
        public void AdjustStock(StockData data, User user)
        {
            // Використання того самого Спільного ядра
            if (!user.HasPermission(new Permission(" Stock", "Adjust")))
                throw new UnauthorizedException();

            // inventory logic...
        }
    }
}

Обмеження області дії (Shared Scope)

Критично важливоСпільне ядро має бути якомога меншим. Ідеально — лише інтеграційні контракти та структури даних.

Чому? Модель з перекриттям зв'язує життєві цикли. Зміна в Спільному ядрі впливає на всі контексти.

Технічна реалізація

Варіант 1: Монорепозиторій

/company-monorepo
  /packages
    /authorization-kernel    ← Спільне ядро
      - User.cs
      - Role.cs
      - Permission.cs
    /sales-context           ← Контекст A
      - SalesOrder.cs
      - ProductCatalog.cs
    /inventory-context       ← Контекст B
      - Warehouse.cs
      - StockItem.cs

Всі контексти посилаються на однакові вихідні файли.

Варіант 2: Окрема бібліотека

# Спільне ядро як NuGet пакет
dotnet pack Authorization Kernel.csproj
dotnet nuget push AuthorizationKernel.1.0.0.nupkg
<!-- У контексті Sales -->
<PackageReference Include="Company.AuthorizationKernel" Version="1.0.0" />

<!-- У контексті Inventory -->
<PackageReference Include="Company.AuthorizationKernel" Version="1.0.0" />

Безперервна інтеграція

Обов'язково!Кожна зміна в Спільному ядрі SHALL запускати інтеграційні тести всіх контекстів, що його використовують.
# GitHub Actions приклад
name: Shared Kernel CI

on:
  push:
    paths:
      - 'packages/authorization-kernel/**'

jobs:
  test-all-contexts:
    runs-on: ubuntu-latest
    steps:
      - name: Test Sales Context
        run: dotnet test packages/sales-context/tests

      - name: Test Inventory Context
        run: dotnet test packages/inventory-context/tests

      - name: Test Reporting Context
        run:ії dotnet test packages/reporting-context/tests

Коли використовувати Спільне ядро

Коли НЕ використовувати

Антипаттерни❌ "Давайте зробимо всю модель спільною!" — Це знищить межі контекстів
❌ Спільне ядро між багатьма (>3) контекстами — Надмірна зв'язаність
❌ Спільне ядро для різних команд без координації — Chaos!
❌ Використання дляBasic моделей (Generic Subdomains) — Краще купити готове рішення

Споживач-Постачальник (Customer-Supplier Patterns)

Друга група паттернів стосується відносин, за яких один контекст (Постачальник, Supplier) надає послуги іншому (Споживач, Customer).

Loading diagram...
graph LR
    U[Upstream<br/>Постачальник] -->|Надає послуги| D[Downstream<br/>Споживач]

    style U fill:#fff3e0
    style D fill:#c8e6c9

Ключова різниця з Cooperation

АспектCooperationCustomer-Supplier
Успіх командВзаємоза лежнийНезалежний
Баланс силРівнийДисбаланс
Хто створює контрактОбидві разомОдна із сторін
АдаптаціяСпільнаОдностороння
assuring Баланс силУ відносинах Consumer-Supplier завжди є дисбаланс сил. Інтеграційний контракт диктує або Постачальник (Upstream), або Споживач (Downstream).

Конформіст (Conformist Pattern)

Conformist — паттерн, за якого Downstream команда приймає модель Upstream команди без змін.

Концепція

Loading diagram...
graph LR
    U[Upstream<br/>Постачальник] -->|Модель| D[Downstream<br/>Конформіст]

    D -.Приймає модель.-> U

    style U fill:#fff3e0
    style D fill:#90caf9
ВизначенняКонформіст — Downstream команда, яка відмовляється від частини своєї автономності і використовує модель Upstream "як є", без перетворень.

Коли це виправдано?

Стандартна модель

Підходяща модель

Зовнішній постачальник

Приклад: Інтеграція з Stripe

Уявімо, що наш контекст Payments інтегрується зі Stripe для обробки платежів.

// Stripe API Model (Upstream)
interface StripeCharge {
    id: string
    amount: number // В центах!
    currency: string
    status: 'succeeded' | 'pending' | 'failed'
    customer: string
    created: number // Unix timestamp
}

// Наш контекст Payments (Downstream - Conformist)
class PaymentService {
    async processPayment(amount: number, currency: string, customerId: string) {
        // Використовуємо модель Stripe безпосередньо
        const charge: StripeCharge = await stripe.charges.create({
            amount: amount * 100, // Конвертуємо в центи
            currency: currency,
            customer: customerId,
        })

        // Зберігаємо заряд у форматі Stripe
        await db.charges.insert({
            stripeId: charge.id,
            amount: charge.amount, // В центах, як у Stripe
            currency: charge.currency,
            status: charge.status, // Використовуємо статуси Stripe
            createdAt: new Date(charge.created * 1000),
        })

        return charge
    }
}

Переваги Conformist

ПлюсиПростота: Немає потреби в перетвореннях
Швидкість розробки: Менше коду
Стабільність: Використання перевіреної моделі
Підтримка: Upstream забезпечує документацію

Недоліки

МінусиВтрата автономності: Модель диктується ззовні
Залежність: Зміни Upstream впливають на Downstream
Незручна модель: Може не ідеально підходити для бізнес-потреб
Обмеження: Неможливість адаптувати під свою Єдину мову

Предохоронний слой (Anti-Corruption Layer Pattern)

ACL (Anti-Corruption Layer) — паттерн, який ізолює Downstream контекст від моделі Upstream через проміжний шар перетворення.

ВизначенняПредохоронний слой — механізм трансляції між моделлю Upstream та моделлю Downstream, який захищає цілісність Єдиної мови Downstream.

Концепція

Loading diagram...
graph LR
    U[Upstream<br/>Постачальник] -->|Модель A| ACL[Предохоронний<br/>Слой]
    ACL -->|Модель B| D[Downstream<br/>Споживач]

    D -.Зах ищений від A.-> ACL

    style U fill:#fff3e0
    style ACL fill:#ffcdd2
    style D fill:#c8e6c9

Коли використовувати ACL?

Приклад: Legacy CRM Integration

Уявімо, що нашій сучасній системі потрібно інтегруватися з застарілою CRM.

Legacy CRM Model (Upstream — лиш):

<!-- Legacy XML Response -->
<Customer_Record>
  < ID>12345</ID>
  <FN>John</FN>
  <LN>Doe</LN>
  <Email_Address>john.doe@example.com</Email_Address>
  <Phone_Num>+1234567890</Phone_Num>
  <Tags>VIP,Premium,Enterprise</Tags>
  <Created_TS>1609459200</Created_TS>
</Customer_Record>

Наша модель (Downstream — чиста):

// Наша чиста модель предметної області
class Customer {
    constructor(
        public readonly id: CustomerId,
        public readonly name: PersonName,
        public readonly email: Email,
        public readonly phone: PhoneNumber,
        public readonly segments: CustomerSegment[],
        public readonly registeredAt: Date,
    ) {}

    isVIP(): boolean {
        return this.segments.includes(CustomerSegment.VIP)
    }
}

// Value Objects
class PersonName {
    constructor(
        public readonly firstName: string,
        public readonly lastName: string,
    ) {}

    get fullName(): string {
        return `${this.firstName} ${this.lastName}`
    }
}

Предохоронний слой:

// ACL: Adapter для Legacy CRM
class LegacyCRMAdapter {
  constructor(private legacyClient: Leg acyCRMClient) {}

  async getCustomer(id: string): Promise<Customer> {
    // Отримуємо дані з Legacy
    const legacyData = await this.legacyClient.fetchCustomerXML(id);

    // Трансляція в нашу модель
    return this.translate(legacyData);
  }

  private translate(legacy: LegacyCustomerXML): Customer {
    return new Customer(
      new CustomerId(legacy.ID),
      new PersonName(legacy.FN, legacy.LN),
      new Email(legacy.Email_Address),
      new PhoneNumber(legacy.Phone_Num),
      this.parseSegments(legacy.Tags),
      new Date(parseInt(legacy.Created_TS) * 1000),
    );
  }

  private parseSegments(tags: string): CustomerSegment[] {
    return tags.split(',').map(tag => {
      switch (tag.trim()) {
        case 'VIP': return CustomerSegment.VIP;
        case 'Premium': return CustomerSegment.PREMIUM;
        case 'Enterprise': return CustomerSegment.ENTERPRISE;
        default: return CustomerSegment.REGULAR;
      }
    });
  }
}

// Використання в додатку
class CustomerService {
  constructor(private crm: LegacyCRMAdapter) {}

  async getCustomerDetails(id: string): Promise<Customer> {
    // Робота з чистою моделлю, незалежно від Legacy!
    const customer = await this.crm.getCustomer(id);

    if (customer.isVIP()) {
      // Бізнес-логіка використовує чисту модель
      await this.апplyVIPDiscount(customer);
    }

    return customer;
  }
}

Структура ACL

PreдохоронПівний слой зазвичай складається з трьох компонентів:

Adapter (Адаптер)

Відповідає за підключення до Upstream API.

class UpstreamAPIAdapter {
    async fetchData(): Promise<UpstreamModel> {
        // HTTP, gRPC, чи інший протокол
    }
}

Translator (Перекладач)

Перетворює модель Upstream у модель Downstream.

class ModelTranslator {
    translate(upstream: UpstreamModel): DownstreamModel {
        // Логіка перетворення
    }
}

Facade (Фасад)

Надає зручний інтерфейс для Downstream контексту.

class UpstreamFacade {
    constructor(
        private adapter: UpstreamAPIAdapter,
        private translator: ModelTranslator,
    ) {}

    async getData(): Promise<DownstreamModel> {
        const upstream = await this.adapter.fetchData()
        return this.translator.translate(upstream)
    }
}

Переваги ACL

Ізоляція

Чиста єдина мова

Гнучкість

Недоліки

ОбмеженняСкладність: Більше коду для підтримки
Продуктивність: Додатковий шар перетворення
Вартість: Потребує більше часу на розробку

Сервіс з Відкритим Протоколом (Open-Host Service Pattern)

OHS (Open-Host Service) — паттерн, за якого Upstream захищає Downstream споживачів, надаючи стабільний, зручний публічний API.

ВизначенняOpen-Host Service — Upstream, який відділяє свою внутрішню модель від публічного інтерфейсу, забезпечуючи стабільний Published Language (опублікований язик).

Концепція

Loading diagram...
graph TB
    subgraph Upstream["Upstream (OHS)"]
        IM[Internal Model]
        PL[Published Language<br/>API v1]
    end

    D1[Downstream 1]
    D2[Downstream 2]
    D3[Downstream 3]

    IM -.Відділено.-> PL
    PL --> D1
    PL --> D2
    PL --> D3

    style Upstream fill:#fff3e0
    style IM fill:#ffcdd2
    style PL fill:#c8e6c9

Published Language

Published Language (Опублікований язик) — це інтерфейс, орієнтований на інтеграцію, а не на внутрішню Єдину мову.

// Внутрішня модель Upstream (НЕ доступна зовні)
class InternalProduct {
    private id: ProductId
    private details: ProductDetails
    private pricing: PricingModel
    private inventory: InventoryTracking

    // Складна internal logic
}

// Published Language (Публічний інтерфейс)
interface ProductAPI {
    id: string
    name: string
    price: {
        amount: number
        currency: string
    }
    availability: 'in_stock' | 'out_of_stock' | 'pre_order'
}

// Upstream API Controller
class ProductController {
    async getProduct(id: string): Promise<ProductAPI> {
        const internal = await repo.find(id)

        // Перекладання Internal Model → Published Language
        return {
            id: internal.id.value,
            name: internal.details.name,
            price: {
                amount: internal.pricing.currentPrice.amount,
                currency: internal.pricing.currentPrice.currency,
            },
            availability: this.mapAvailability(internal.inventory.status),
        }
    }
}

Versioning: Підтримка кількох версій

Одна з переваг OHS — можливість підтримки кількох версій API одночасно.

Loading diagram...
graph TB
    subgraph Upstream["Upstream (OHS)"]
        IM[Internal Model]
        PLv1[Published Language v1]
        PLव2[Published Language v2]
    end

    D1[Old Downstream]
    D2[New Downstream]

    IM --> PLv1
    IM --> PLv2
    PLv1 --> D1
    PLव2 --> D2

    style Upstream fill:#fff3e0
    style IM fill:#ffcdd2
    style PLv1 fill:#90caf9
    style PLव2 fill:#c8e6c9
// API v1 (Legacy)
app.get('/api/v1/products/:id', async (req, res) => {
    const product = await service.getProduct(req.params.id)

    res.json({
        product_id: product.id,
        product_name: product.name,
        price_amount: product.price.amount,
    })
})

// API v2 (Новий)
app.get('/api/v2/products/:id', async (req, res) => {
    const product = await service.getProduct(req.params.id)

    res.json({
        id: product.id,
        name: product.name,
        pricing: product.price,
        availability: product.availability,
        metadata: {
            version: '2.0',
            lastUpdated: new Date().toISOString(),
        },
    })
})

Порівняння: ACL vs OHS

АспектACLOHS
Хто перекладаєDownstream (Споживач)Upstream (Постачальник)
Баланс силUpstream сильнішийDownstream сильніший
ВідповідальністьСпоживач захищає себеПостачальник захищає споживачів
СкладністьУ DownstreamУ Upstream
Цікавий фактOHS — це "перевернутий" ACL. Обидва паттерни вирішують проблему перекладу моделей, але відповідальність лежить на різних боках.

Різні Шляхи (Separate Ways Pattern)

Separate Ways — паттерн повного відмови від інтеграції. Команди вирішують працювати незалежно і дублюють функціональність.

ВизначенняSeparate Ways — стратегічне рішення про те, що вартість інтеграції перевищує вигоди, і команди вирішують рухатися різними шляхами.

Коли це виправдано?

Приклад: Логування

// Контекст Orders
import winston from 'winston';

const logger = winston.create Logger();

class OrderService {
  createOrder(data: OrderData) {
    logger.info('Order created', { orderId: data.id });
    // ...
  }
}

// Контекст Payments
import pino from 'pino';

const logger = pino();

class PaymentService {
  processPayment(data: PaymentData) {
    logger.info({ paymentId: data.id }, 'Payment processed');
    // ...
  }
}

Обидва контексти використовують різні бібліотеки логування, кожна інтегрована локально.

Обмеження

Не використовуйте для Core Subdomains!Дублювання реалізації Core Subdomains суперечить бізнес-стратегії компанії. Core має бути оптимізованим та кращіdifferent.

Карта контекстів (Context Map)

Context Map — візуальне представлення Обмежених контекстів системи та їх інтеграцій.

Loading diagram...
graph TB
    subgraph Platform["E-Commerce Platform"]
        Catalog[Product Catalog]
        Orders[Order Management]
        Payments[Payment Processing]
        Shipping[Shipping & Delivery]
        CRM[Customer Relations]
    end

    Catalog -->|OHS| Orders
    Orders -->|Partnership| Payments
    Orders -->|Partnership| Shipping
    CRM -->|ACL| Orders
    Payments -.Separate Ways.-> Shipping

    style Catalog fill:#fff3e0
    style Orders fill:#c8e6c9
    style Payments fill:#bbdefb
    style Shipping fill:#ffcdd2
    style CRM fill:#fff9c4

Цінність Context Map

Високорівневе бачення

Модці комунікації

Організаційні проблеми

Приклад реальної Context Map

[Product Catalog] --OHS--> [Order Management]
                           |
                           +--Partnership--> [Payment Processing]
                           |
                           +--Partnership--> [Shipping & Delivery]

[Customer CRM] --ACL--> [Order Management]

[Analytics] --Conformist--> [Order Management]
           --Conformist--> [Product Catalog]

[Inventory] --Separate Ways-- [Shipping]

Підтримка Context Map

Best Practices
  1. Як код: Використовуйте Context Mapper або PlantUML
  2. Living Document: Оновлюйте при змінах
  3. Shared Responsibility: Кожна команда оновлює свої інтеграції
  4. Version Control: Зберігайте в Git

Резюме та ключові думки

У цій главі ми вивчили паттерни інтеграції Обмежених контекстів, класифіковані за типом командної співпраці.

Ключові висновки

typeПаттерни співпраці (Cooperation)

  • Partnership: Двостороння координація, щільна співпраця
  • Shared Kernel: Спільна модель, використовувати обережно

Споживач-Постачальник (Customer-Supplier)

  • Conformist: Downstream приймає модель Upstream
  • ACL: Downstream захищає себе через трансляцію
  • OHS: Upstream захищає Downstream через Published Language

Різні офілії (Separate Ways)

  • Відмова від інтеграції, дублювання функціональності
  • НЕ використовувати для Core Subdomains!

Context Map

  • Візуалізація інтеграцій та командних зв'язків
  • Інструмент стратегічного аналізу

Вибір паттерну

Зв

'язок з наступними темами

Тепер, коли ми розуміємо, як интегруються Обмежені контексти на стратегічному рівні, наступним кроком є вивчення тактичного проектування — як реалізовувати бізнес-логіку всередині контексту.

Наступні крокиУ наступних главах розглянемо:
  • Паттерни реалізації бізнес-логіки (Transaction Script, Domain Model)
  • Architectурні паттерни (Layered, Hexagonal, CQRS)
  • Технічну інтеграцію (Messaging, Sagas)
Copyright © 2026