Коли продукт складається з одного бекенду й однієї бази даних, синхронний REST-виклик — цілком робочий спосіб зв'язати компоненти між собою. Але щойно сервісів стає більше, пряме звертання одного сервісу до іншого починає коштувати дорожче, навіть із таймаутами, ретраями й circuit breaker доводиться окремо продумувати, що станеться з усім ланцюжком викликів, якщо один із сервісів деградує. Event-Driven Architecture (EDA) пропонує інший принцип: сервіси не чекають на відповідь одне одного, а асинхронно обмінюються подіями — фактами про те, що в системі щось сталося.
Про таку архітектуру розповідає Владислав Пістун, Software Engineer у Solidgate. Він розбирає, з чого складається Event-Driven Architecture, як у ній працює pub/sub, чим event bus відрізняється від message broker і коли перехід на такий підхід виправданий, а коли — ні. Ключові компоненти він ілюструє прикладом із власної практики — сервісами оплат і розрахунку податків.
Що таке Event-Driven Architecture простими словами
Event-Driven Architecture — підхід, у якому компоненти взаємодіють не напряму, а через публікацію та обробку подій. Це незмінний факт про те, що в системі вже відбулася зміна стану: наприклад, order.created або transaction.created. Замість синхронного HTTP-виклику продюсер публікує факт події, а зацікавлені консюмери самостійно її зчитують — кожен у свій час і за власною логікою.
З яких компонентів складається event-driven система
Producer. Фіксує факт зміни стану й публікує подію, не знаючи, хто саме її прочитає.
Consumer. Підписується на топік і реагує на нові події своєю логікою.
Event. Незмінне повідомлення про факт. Передається або як мінімальна нотифікація (лише ID), або як повний payload у JSON чи Protobuf.
Event Transport. Проміжний інфраструктурний компонент, який приймає події від продюсерів і доставляє їх консюмерам. Конкретна механіка різниться залежно від типу: класичний брокер черг (RabbitMQ) організовує повідомлення в черги, а лог-орієнтована стрімінгова платформа (Kafka, як у прикладі нижче) — у топіки з партиціями.
Local Storage / State Cache. Консюмер зберігає отримані дані у власній локальній базі, щоб не робити зворотних викликів до продюсера.
Приклад:
«Як практичний кейс можна взяти сервіс розрахунку податків (Tax-сервіс). Його суть полягає в тому, щоб у реальному часі розраховувати ПДВ для Європи чи Sales Tax для США для кожної транзакції, для чого йому потрібні деталі замовлення — країна клієнта, тип продукту та сума.
У такій схемі Payment-сервіс публікує дві події у два окремі топіки: order.created, коли створюється нове замовлення (де є повні дані замовлення), та transaction.created, коли відбувається сама транзакція. Кластер Kafka зберігає обидва топіки з партиціями для паралельної обробки.
Сам Tax-сервіс має двох незалежних консюмерів. Перший обробляє подію order.created та зберігає замовлення в локальну базу Postgres у форматі JSONB — це наш локальний кеш стану. Другий консюмер слухає transaction.created, зчитує потрібні дані з цього кешу, розраховує податок і публікує результат.
Kafka у цьому випадку — це транспорт, а Postgres — це сховище даних. Ключовий момент полягає в тому, що зв'язок між консюмерами відбувається асинхронно через базу даних, а продюсер стає повністю ізольованим одразу після публікації своїх подій», — пояснює Владислав Пістун.
4 основні патерни Event-Driven Architecture
«Коли люди говорять про події, вони насправді можуть мати на увазі дуже різні речі. Наприклад, дві команди домовляються перейти на Event-Driven Architecture, а через пів року виявляється, що вони будували зовсім різні системи: одні реалізовували Event Notification, а інші — Event Sourcing. А це принципово різні архітектурні рішення», — казав Мартін Фаулер під час відомого виступу «The Many Meanings of Event-Driven Architecture» (GOTO 2017).
Щоб уникнути такої плутанини, Фаулер виділяє чотири основні патерни:
Event Notification
Суть: подія несе мінімум даних (лише ID); деталі — окремим callback-запитом до джерела. Розвертає напрямок залежності між сервісами
Використовувати для: простого decoupling
Приклад: email при створенні замовлення
Компроміс: додатковий мережевий трафік, залежність від доступності джерела
Event-Carried State Transfer (ECST)
Суть: подія несе повний стан об'єкта; консюмер кешує його локально, без callback-ів
Використовувати для: високої доступності, швидких локальних читань
Приклад: розрахунок податків, синхронізація каталогу
Компроміс: дубльовані дані, eventual consistency
Event Sourcing
Суть: зберігається повна незмінна послідовність подій; стан обчислюється через replay
Використовувати для: повного аудиторського сліду, комплаєнсу
Приклад: банківські транзакції, медичні записи
Компроміс: незвичний для більшості розробників спосіб мислення; додатково — еволюція схеми подій роками наперед, потреба у снепшотах для швидкого replay при великій історії й конфлікт з GDPR: з незмінного логу неможливо просто видалити дані конкретної людини
CQRS
Суть: окремі моделі запису й читання; читання денормалізоване під конкретні запити
Використовувати для: складних сценаріїв читання, важких запитів
Приклад: пошук в e-commerce, аналітичні дашборди
Компроміс: асинхронна, одностороння проєкція подій із write-моделі в read-модель(і) — звідси затримка й eventual consistency між записом і читанням
«Використовувати підхід Event-Carried State Transfer потрібно тоді, коли для нас критична висока доступність — сервіс має продовжувати працювати, навіть якщо сервіси-джерела тимчасово недоступні. А також у випадках, коли необхідні швидкі локальні операції читання або коли сервіс-джерело є високонавантаженим і ми не можемо спрямовувати до нього тисячі додаткових запитів. Наприклад, Netflix зберігає метадані фільмів локально у кожному регіональному сервісі за допомогою ECST, що дає змогу виконувати пошук контенту навіть тоді, коли центральний каталог повністю недоступний.
Інший приклад — Uber, який використовує патерн CQRS: їхня Write-модель обробляє замовлення через PostgreSQL, тоді як Read-модель окремо оптимізована під швидкий пошук та фільтрацію ресторанів.
Загалом ці чотири патерни Event-Driven Architecture вирішують абсолютно різні завдання і не є взаємовиключними. Головна практична порада — починати з найпростішого патерну, який задовольняє ваші поточні вимоги, і не намагатися одразу будувати CQRS разом із Event Sourcing та Event-Carried State Transfer», — підсумовує Владислав Пістун.
Як працює pub/sub у Event-Driven архітектурі
Незалежно від того, який із цих чотирьох патернів обрано, події зазвичай доставляються за моделлю Publish-Subscribe — вона повністю розмежовує відправника й отримувачів. Продюсер публікує подію в топік, не знаючи, скільки сервісів її прочитають; будь-який консюмер підписується на топік і зчитує нові події у власному темпі. Головна перевага — легкість розширення: щоб додати новий бізнес-процес, достатньо підписати новий консюмер, не змінюючи код продюсера.
«У цьому випадку Order-сервіс взагалі не знає про існування інших сервісів. Новий сервіс просто підписується на топік, і ми нічого не змінюємо в продюсері, що робить цей підхід дуже простим для розширення», — пояснює Владислав Пістун.
На відміну від Point-to-Point, де повідомлення розбирають консюмери-конкуренти й кожне дістається лише одному з них, pub/sub — це fan-out: кожен підписник отримує власну копію кожної події. Це питання конфігурації, а не властивість конкретного брокера: у Kafka кілька інстансів одного сервісу в одній consumer group ділять партиції між собою — це Point-to-Point, а кілька різних сервісів зі своїми окремими consumer groups, що читають той самий топік незалежно, — це вже pub/sub.
Event bus і message broker: у чому різниця
Message Broker (RabbitMQ, ActiveMQ) — чергування й точна маршрутизація до конкретних отримувачів. Повідомлення живе в черзі, поки консюмер його не обробить і не підтвердить; якщо цього не сталося, брокер поверне повідомлення в чергу, а після кількох невдалих спроб — в окрему чергу для проблемних повідомлень (dead-letter queue), уже вбудовану в сам брокер.
Event Bus (Kafka, AWS Kinesis) — незмінний розподілений лог (append-only), де події зберігаються протягом retention незалежно від того, чи їх уже прочитали. Це дозволяє кільком незалежним сервісам читати той самий потік зі своєю швидкістю та за потреби повторно програвати історію (replay). Dead-letter queue тут — не вбудована функція платформи, а патерн, який команда реалізує сама.
Критерій | Message Broker (RabbitMQ) | Event Bus (Kafka) |
Зберігання | До ACK, потім видаляється | Retention незалежно від прочитання |
Replay | Немає | Так |
Порядок доставки | У межах черги, якщо її читає один консюмер | У межах партиції; між партиціями й топіками не гарантований |
Типовий сценарій | Точкові задачі, фонова обробка | Стрімінг, кілька незалежних читачів одного потоку |
Коли продукту потрібна Event-Driven Architecture
Слабкий зв'язок (decoupling). Продюсер публікує подію, а будь-яка кількість інших сервісів читає її без зміни коду джерела.
Висока доступність. Сервіси продовжують приймати запити, навіть коли частина компонентів тимчасово недоступна.
Високе навантаження. Передача стану через події знімає з джерела зайві API-виклики від інших сервісів.
Незалежне масштабування консюмерів, що стали вузьким місцем.
Реактивність у реальному часі замість періодичного опитування (polling).
«Перш ніж відповідати на питання «як», варто зрозуміти «навіщо» і які переваги надає Event-Driven Architecture. Насамперед це Decoupling (послаблення зв'язаності) — я вважаю найбільшою перевагою, адже сервіси взагалі не знають про існування один одного. Звідси випливає і можливість їхнього незалежного масштабування», — пояснює Владислав.
Коли Event-Driven Architecture створює зайву складність
Невеликі продукти й прості CRUD-системи — синхронні REST-запити простіші й ефективніші.
Eventual consistency — дані оновлюються з затримкою; для суворої консистентності в реальному часі (наприклад, перевірка балансу перед списанням) це проблема.
Складність дебагінгу без наскрізного розподіленого трейсингу.
Out-of-order події. Дві пов'язані події можуть прийти консюмеру в іншому порядку, ніж сталися насправді, — гарантія порядку в більшості систем діє лише в межах одного каналу доставки, а не між різними. Типове рішення — почекати й повторити спробу з експоненційним бекоффом, а якщо залежна подія так і не з'явилась — відправити в dead-letter queue.
Незадокументована модель консистентності й брак досвіду в команді.
Інфраструктурні витрати на підтримку власного брокера чи стрімінгової платформи.
«Event-Driven Architecture значно складніша для розуміння й підтримки, ніж прості синхронні HTTP-виклики. Це не срібна куля: для невеликого застосунку на 2-3 сервіси вона може стати невиправданим ускладненням», — зазначає Владислав Пістун.
Event-Driven Architecture і мікросервіси
Мікросервіси й EDA нерідко плутають, бо на практиці одне тягне за собою інше: розбивши систему на окремі сервіси, рано чи пізно доводиться вирішувати, як їм спілкуватись. Але це не синоніми: мікросервіси — про структурний поділ на бізнес-домени, EDA — про спосіб асинхронної комунікації між ними.
За синхронного підходу сервіси ризикують падати каскадом одне за одним, а виклики між ними лишаються приховано тісно зв'язаними навіть попри поділ на окремі домени.
За словами Владислава, якщо використовувати традиційний синхронний підхід, виникає мережева затримка та ризик каскадних збоїв: якщо Payment-сервіс деградує, Tax-сервіс також зазнає збою. Крім того, зберігається прихована тісна зв'язаність (tight coupling) через зворотні API-запити.
EDA цьому запобігає ще й через чіткіший розподіл відповідальності між сервісами.
Практичний наслідок цього підходу — свідома денормалізація: кожен мікросервіс тримає власну копію потрібних йому даних, а не тягнеться по мережі за чужою базою.
«Тут дуже влучна цитата Вернера Фогельса з Amazon: "Denormalize because the alternative is distributed joins". І це правда, адже розподілені джойни між мікросервісами — це значно гірша й складніша альтернатива», — підсумовує Владислав Пістун.
FAQ
Що таке event bus?
Канал, що доставляє опубліковані події всім зацікавленим підписникам (pub/sub). У Kafka чи Kinesis він додатково зберігає історію подій і дозволяє її повторно прочитати — але це вже конкретна реалізація, а не обов'язкова ознака будь-якого event bus.
Для чого потрібен message broker?
Розділяє продюсера й консюмера в часі та гарантує доставку через підтвердження (ACK) — незалежно від того, чи повідомлення читає один консюмер, чи кілька.
Як працює pub/sub?
Продюсер публікує подію в топік, не знаючи, хто її прочитає; будь-який сервіс підписується й зчитує події у власному темпі, не змінюючи код продюсера.
Коли варто переходити на Event-Driven Architecture?
Коли багато незалежних сервісів, критична висока доступність, високе навантаження на джерело даних або потрібна реакція в реальному часі. Для 2-3 сервісів і простих CRUD EDA зазвичай надлишкова.


















