Пошук уроків, статей та іншого контенту
Будуємо взаємодію через події, розглядаємо брокери й узгодженість та визначаємо межі доцільності підходу.
Подієво-орієнтована архітектура, або Event-Driven Architecture (EDA), будує взаємодію між компонентами через події.
Подія — це факт, який уже відбувся:
OrderCreated — замовлення створено;
PaymentAuthorized — платіж авторизовано;
UserRegistered — користувача зареєстровано;
FileUploaded — файл завантажено.
Подія описує не команду, а результат дії. Порівняйте:
команда: «заплати за замовлення»;
подія: «платіж за замовлення авторизовано».
Команда зазвичай адресована конкретному обробнику, а подію можуть отримувати кілька незалежних споживачів.
Наприклад, після створення замовлення можуть реагувати:
сервіс оплати;
сервіс резервування товару;
сервіс сповіщень;
сервіс аналітики.
Сервіс замовлень не обов’язково має викликати кожен із них напряму.
У типовій подієвій системі є такі компоненти.
Видавець
Видавець не повинен залежати від внутрішньої реалізації споживачів. Йому достатньо знати контракт події.
Брокер приймає, зберігає та доставляє події споживачам.
Залежно від технології брокер може підтримувати:
черги повідомлень;
теми або канали;
повторну доставку;
підтвердження обробки;
зберігання повідомлень;
маршрутизацію;
групи споживачів.
У навчальному прикладі роль брокера може виконувати звичайний об’єкт у пам’яті. У production-системі використовують спеціалізовані брокери, конфігурація та гарантії яких залежать від конкретної технології.
Споживач отримує подію та виконує свою частину роботи.
Одна подія може мати кілька незалежних споживачів. Наприклад:
OrderCreated
├── PaymentService
├── InventoryService
└── NotificationServiceЦе зменшує зв’язаність між сервісами: сервіс замовлень не повинен знати, скільки інших сервісів реагує на OrderCreated.
Розглянемо оформлення замовлення:
Клієнт
│
▼
Order Service
│ публікує OrderCreated
▼
Брокер
├── Payment Service
├── Inventory Service
└── Notification ServiceПослідовність може бути такою:
Order Service створює замовлення.
Order Service публікує OrderCreated.
Брокер доставляє подію кільком споживачам.
Payment Service починає авторизацію платежу.
Inventory Service резервує товар.
Notification Service надсилає повідомлення користувачу.
Ці операції можуть виконуватися асинхронно. Order Service не зобов’язаний чекати завершення всіх споживачів перед відповіддю клієнту.
Подія має містити достатньо інформації для споживача, але не повинна бути випадковою копією внутрішньої моделі видавця.
Приклад події:
{
"id": "event-123",
"type": "OrderCreated",
"occurredAt": "2026-09-02T10:15:00.000Z",
"version": 1,
"data": {
"orderId": "order-42",
"customerId": "customer-7",
"total": 1250,
"currency": "UAH"
}
}Корисні поля:
id — унікальний ідентифікатор події;
type — тип події;
occurredAt — час виникнення;
version — версія контракту;
data — дані події.
Подія має бути зрозумілою без читання внутрішнього коду видавця. Її структура стає контрактом між видавцем і споживачами.
Команда і подія мають різний зміст.
Команда виражає намір:
AuthorizePayment
ReserveInventory
SendWelcomeEmailЇї зазвичай надсилають конкретному виконавцю. Команда може бути відхилена.
Подія виражає факт:
PaymentAuthorized
InventoryReserved
WelcomeEmailSentПодія описує те, що вже сталося. Вона може запускати кілька незалежних реакцій.
В окремій системі можуть використовуватися і команди, і події. Важливо не називати команду подією лише тому, що вона передається через брокер.
Нижче наведено невеликий брокер у пам’яті. Він демонструє:
публікацію події;
підписку кількох споживачів;
асинхронну обробку;
ідемпотентність споживача.
Цей приклад призначений для розуміння принципу. Після завершення процесу всі дані зникнуть, тому такий брокер не підходить для production.
class InMemoryBroker {
constructor() {
this.handlers = new Map();
}
subscribe(eventType, handler) {
const handlers = this.handlers.get(eventType) ?? [];
handlers.push(handler);
this.handlers.set(eventType, handlers);
}
async publish(event) {
const handlers = this.handlers.get(event.type) ?? [];
await Promise.all(
handlers.map(async (handler) => {
await handler(event);
})
);
}
}
const broker = new InMemoryBroker();
const processedPaymentEvents = new Set();
broker.subscribe("OrderCreated", async (event) => {
const { orderId, total, currency } = event.data;
console.log(
`[PaymentService] Авторизація платежу: ${orderId}, ${total} ${currency}`
);
await new Promise((resolve) => setTimeout(resolve, 100));
const paymentEvent = {
id: `payment-${event.data.orderId}`,
type: "PaymentAuthorized",
occurredAt: new Date().toISOString(),
version: 1,
data: {
orderId,
total,
currency
}
};
await broker.publish(paymentEvent);
});
broker.subscribe("OrderCreated", async (event) => {
const { orderId } = event.data;
console.log(`[NotificationService] Надсилання підтвердження для ${orderId}`);
});
broker.subscribe("PaymentAuthorized", async (event) => {
if (processedPaymentEvents.has(event.id)) {
console.log(
`[OrderService] Подію ${event.id} уже оброблено, пропускаємо повтор`
);
return;
}
processedPaymentEvents.add(event.id);
console.log(
`[OrderService] Замовлення ${event.data.orderId} позначено як оплачене`
);
});
const orderCreatedEvent = {
id: "event-order-42-created",
type: "OrderCreated",
occurredAt: new Date().toISOString(),
version: 1,
data: {
orderId: "order-42",
customerId: "customer-7",
total: 1250,
currency: "UAH"
}
};
await broker.publish(orderCreatedEvent);
// Імітуємо повторну доставку тієї самої події.
await broker.publish({
id: "payment-order-42",
type: "PaymentAuthorized",
occurredAt: new Date().toISOString(),
version: 1,
data: {
orderId: "order-42",
total: 1250,
currency: "UAH"
}
});Очікувана властивість цього прикладу: повторна доставка PaymentAuthorized не повинна вдруге змінити стан замовлення.
Подієва взаємодія часто є асинхронною. Це означає, що після публікації події інші компоненти можуть обробити її пізніше.
Наприклад:
замовлення створено;
відповідь клієнту вже надіслано;
платіж ще обробляється;
сповіщення буде надіслано через декілька секунд.
Тому система стає слабо узгодженою, або eventually consistent.
Це не означає, що дані неправильні. Це означає, що різні частини системи можуть тимчасово бачити різні стани.
Наприклад:
Order Service: CREATED
Payment Service: PENDING
Notification Service: ще не обробив подіюЧерез деякий час стани узгодяться:
Order Service: PAID
Payment Service: AUTHORIZED
Notification Service: повідомлення надісланоВона часто підходить для:
надсилання електронної пошти;
побудови аналітики;
оновлення пошукового індексу;
створення аудиту;
обробки фонових задач;
формування рекомендацій;
синхронізації некритичних проєкцій.
Якщо користувачеві необхідно негайно отримати остаточний результат, асинхронної події може бути недостатньо.
Наприклад:
перевірка доступу перед виконанням критичної операції;
миттєва валідація обов’язкових даних;
операція, де відповідь одного сервісу потрібна для продовження поточного запиту.
У таких випадках можна використати синхронний виклик, а подію опублікувати після успішної операції для подальших реакцій.
Брокер може надавати різні гарантії доставки повідомлень.
Повідомлення доставляється не більше одного разу.
Перевага:
немає повторної обробки.
Недолік:
повідомлення може бути втрачено.
Такий режим може бути прийнятним для некритичних метрик або тимчасових повідомлень.
Повідомлення доставляється щонайменше один раз.
Перевага:
брокер намагається не втратити подію.
Недолік:
одна подія може бути доставлена повторно.
Це поширений компроміс, але він вимагає ідемпотентних споживачів.
Подія обробляється рівно один раз.
На практиці таку гарантію складно забезпечити на всьому шляху: між брокером, базою даних і зовнішніми сервісами можуть виникати помилки. Тому прикладні системи часто будують із припущенням про повторну доставку.
Ідемпотентна операція дає той самий результат, якщо виконати її один або кілька разів.
Наприклад, замість операції:
збільшити баланс на 100небезпечної при повторі, можна використати:
встановити статус платежу event-123 як AUTHORIZEDПовторне встановлення такого самого статусу не змінить результат.
Поширені способи забезпечити ідемпотентність:
зберігати ідентифікат уже обробленої події;
використовувати унікальний ключ у базі даних;
виконувати оновлення за умови, наприклад WHERE status != 'PAID';
передавати стабільний ключ операції зовнішньому сервісу;
проєктувати зміни як встановлення стану, а не безумовне збільшення.
Споживач має позначати подію обробленою атомарно з основною зміною стану. Інакше можливий такий сценарій:
споживач змінив дані;
процес завершився з помилкою до збереження факту обробки;
брокер повторно доставив подію;
зміна виконалася вдруге.
Асинхронний споживач може завершитися помилкою через:
тимчасову недоступність бази даних;
мережеву помилку;
перевищення ліміту зовнішнього API;
некоректні дані події;
помилку програмного коду.
Тому система має визначити:
скільки разів повторювати обробку;
інтервал між спробами;
які помилки є тимчасовими;
що робити з подією після невдалих спроб.
Для повідомлень, які не вдалося обробити, часто використовують dead-letter queue — окрему чергу проблемних повідомлень. Вона дає змогу не блокувати інші події та дослідити причину помилки.
Без обмеження кількості повторів одна некоректна подія може створити нескінченний цикл невдалих спроб.
У розподіленій системі не варто автоматично припускати, що всі події будуть оброблені в потрібному порядку.
Наприклад, споживач може отримати:
OrderCancelled
OrderCreatedзамість очікуваного порядку через затримки або повторну доставку.
Якщо порядок має значення, потрібно явно передбачити механізм:
порядковий номер події;
версію стану;
ключ партиціювання;
перевірку попереднього стану;
повторну обробку події, яка прийшла зарано.
Події одного агрегата, наприклад одного замовлення, часто прив’язують до одного ключа. Це може допомогти зберігати порядок у межах конкретного ключа, але не створює глобального порядку для всієї системи.
Є важлива проблема: сервіс може успішно змінити базу даних, але не встигнути опублікувати подію.
Наприклад:
Order Service записав замовлення в базу;
процес завершився до публікації OrderCreated;
інші сервіси ніколи не дізналися про замовлення.
Або навпаки:
подію опубліковано;
транзакція бази даних відкотилася;
споживачі отримали подію про об’єкт, якого фактично немає.
Для узгодження запису в базу та майбутньої публікації часто застосовують патерн Outbox:
у межах однієї транзакції зберігають основні дані та подію в таблицю outbox;
окремий процес читає події з outbox;
цей процес публікує їх у брокер;
після успішної публікації подія позначається як відправлена.
Це не усуває повторну доставку, тому споживачі все одно мають бути ідемпотентними.
Подієво-орієнтована архітектура може дати такі переваги:
слабше зв’язування між компонентами;
незалежне масштабування споживачів;
додавання нових реакцій без зміни видавця;
асинхронне виконання довгих операцій;
буферизація навантаження через черги;
можливість повторно обробляти збережені події;
зручне формування аудиту та історії змін.
Наприклад, щоб додати аналітичний сервіс, не обов’язково змінювати сервіс замовлень. Достатньо підписати новий сервіс на вже наявні події, якщо їхній контракт містить потрібні дані.
Подієвий підхід не є безкоштовним. Він додає:
затримки між змінами стану;
складніше трасування запитів;
повторну доставку;
проблеми з порядком подій;
необхідність версіонування контрактів;
складнішу обробку помилок;
потребу в моніторингу брокера та споживачів.
Стан системи може бути розподілений між кількома сервісами. Щоб зрозуміти, чому замовлення має певний статус, іноді потрібно проаналізувати ланцюжок подій, а не один синхронний виклик.
Подієва архітектура доречна, коли:
є кілька незалежних реакцій на одну зміну;
операції можуть виконуватися асинхронно;
тимчасова затримка узгодження прийнятна;
система має нерівномірне або пікове навантаження;
компоненти потрібно масштабувати незалежно;
події мають цінність як історія фактів або інтеграційний контракт.
Підхід особливо корисний у системах, де один бізнес-факт запускає багато процесів.
Не варто додавати брокер і асинхронні події лише заради модного архітектурного стилю.
Простий синхронний виклик може бути кращим, якщо:
система невелика;
є лише один споживач;
результат потрібен негайно;
немає потреби в буферизації;
команда не готова підтримувати брокер і моніторинг;
затримка або слабка узгодженість неприйнятні;
взаємодія має простий лінійний сценарій.
Надмірна кількість подій може перетворити систему на важку для розуміння мережу неявних залежностей.
Практичне правило: використовуйте події там, де вони зменшують зв’язаність або вирішують конкретну проблему масштабування, надійності чи асинхронності.
Назва ProcessOrder звучить як команда, а не як факт. Для події краще використовувати назву в минулому часі:
OrderCreated;
PaymentAuthorized;
OrderCancelled.
Не можна припускати, що кожна подія буде доставлена рівно один раз. Повторна обробка має бути безпечною.
Споживач не повинен мовчки працювати з подіями в неправильному порядку. Потрібні версії, номери послідовності або перевірка стану.
Подія не має містити всю базу даних або внутрішні об’єкти сервісу. Великі повідомлення збільшують зв’язаність і вартість передачі.
Якщо споживачі очікують поле total, його не можна без плану видалити або змінити тип. Для важливих змін використовують версії подій або сумісне розширення контракту.
Якщо зміна в базі та публікація події виконуються незалежно, одна з операцій може не відбутися. Для критичних процесів слід розглянути Outbox.
Для діагностики потрібні щонайменше:
ідентифікатор події;
ідентифікатор кореляції;
час створення та обробки;
кількість повторних спроб;
тривалість обробки;
інформація про помилку.
Без цього складно відновити шлях події через кілька сервісів.
Подієво-орієнтована архітектура організовує взаємодію через факти, що вже відбулися.
Видавець публікує подію, брокер доставляє її, а споживач виконує реакцію.
Одна подія може мати кілька незалежних споживачів.
Асинхронність зазвичай означає eventual consistency та тимчасово різні стани в сервісах.
Потрібно враховувати повторну доставку, порядок подій і помилки обробки.
Споживачі мають бути ідемпотентними.
Для надійного зв’язку між транзакцією бази та публікацією події часто використовують Outbox.
Події доцільні, коли вони вирішують реальну проблему зв’язаності, масштабування або асинхронної обробки.
Для простої взаємодії синхронний виклик може бути зрозумілішим і дешевшим у підтримці.