Модуль 2

Message-driven архітектура та use cases

Ось твій урок. Вдягай зручне худі, бери каву — ми починаємо! ☕️💻


🎓 УРОК: Message-driven архітектура та Use Cases

(або чому ми перестали телефонувати і почали писати в месенджери)


1. 🔥 Вступ: Проблема «зайнятого касира»

Уявіть, що ви зайшли в Starbucks (або вашу улюблену кав'ярню) вранці понеділка. Черга величезна.

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

Звучить як жахливий сервіс, правда?

Але саме так працює більшість класичних веб-додатків (монолітів) зі синхронною архітектурою. Користувач натискає кнопку «Оформити замовлення» — і сервер починає: 1. Знімати гроші з картки... (чекаємо відповідь банку) 2. Генерувати PDF-чек... (процесор напружився) 3. Відправляти Email... (ой, поштовий сервер завис на 3 секунди) 4. Писати в базу даних...

Якщо на кроці 3 виникла помилка — все замовлення падає. Клієнт бачить спінер завантаження 10 секунд, а потім — 500 Error.

Риторичне питання: Чи має касир сам варити каву, поки ви чекаєте? Чи його робота — просто прийняти замовлення і крикнути баристі: «Одне лате!»?

Сьогодні ми розберемо Message-driven архітектуру. Це спосіб, який дозволяє вашій системі працювати як ідеальна кухня ресторану: швидко, асинхронно і надійно.


2. 🧠 Теоретична база (Як це працює під капотом)

Давай розкладемо магію на прості деталі.

У звичайному коді (Use Case) один модуль викликає інший прямо: OrderService $\rightarrow$ EmailService. Це як телефонний дзвінок: якщо ніхто не бере слухавку, розмова не відбудеться.

У Message-driven світі ми не дзвонимо. Ми надсилаємо повідомлення (як SMS або лист).

Ключові гравці:

  1. Producer (Виробник): Той, хто створює подію. (У кав'ярні — це касир, який друкує чек).
  2. Message (Повідомлення): Дані про те, що сталося. Це маленький пакет інформації. Наприклад: {"orderId": 123, "coffee": "latte"}.
  3. Broker / Message Queue (Пошта / Дошка замовлень): Це посередник. RabbitMQ, Kafka, Amazon SQS. Це місце, де повідомлення лежать і чекають, поки хтось їх забере.
  4. Consumer (Споживач): Той, хто виконує роботу. (Бариста). Він постійно питає Брокера: «Є щось для мене?».

Як це змінює Use Cases?

Раніше твій Use Case називався CreateOrder і робив ВСЕ. Тепер твій Use Case розбивається на частини:

  1. Command (Команда): «Створи замовлення». Це намір.
  2. Event (Подія): «Замовлення створено». Це факт, що вже стався.

❗️ Що треба запам’ятати залізобетонно: Команди (Commands) — це наказ зробити щось конкретне (наприклад, SendEmail). Вони можуть провалитися. Події (Events) — це повідомлення про те, що щось вже сталося (UserRegistered). На події ми просто реагуємо.

Інтуїтивне розуміння:

Message-driven — це про розчеплення (decoupling). Касиру байдуже, скільки барист на зміні, чи працює кавомашина, чи закінчилося молоко. Він просто клеїть стікер (Message) на дошку. Його робота зроблена.


3. 🧪 Приклади (Код і логіка)

Давай подивимось, як це виглядає в архітектурі.

Приклад 1: "Hello World" (Синхронно vs Асинхронно)

Синхронно (Погано для важких задач):

def register_user(user_data):
    save_to_db(user_data)
    send_welcome_email(user_data) # <--- Якщо тут помилка або затримка, юзер чекає
    return "Success"

Message-driven (Добре):

# Producer (API)
def register_user(user_data):
    save_to_db(user_data)
    # Ми просто кидаємо подію в чергу. Це займає 0.001 сек.
    message_bus.publish("UserRegistered", user_data) 
    return "Success" # Юзер миттєво отримує відповідь!

# Consumer (десь в іншому процесі)
def handle_user_registered(event):
    # Цей код може виконатися через 1 сек, 5 сек або навіть завтра
    send_welcome_email(event.user_data)

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

Відповідь: Ні! Користувач вже отримав "Success". Повідомлення залишиться в черзі (Broker), і коли поштовий сервіс підніметься — він його обробить. Система стала стійкою (resilient).


Приклад 2: Use Case в реальному житті (E-commerce)

Уяви Use Case: Купівля квитка в кіно.

Сценарій: 1. Користувач обирає місце. 2. Система бронює місце. 3. Система списує гроші. 4. Система генерує квиток.

Як ми проектуємо це через повідомлення?

  1. API: Отримує запит -> відправляє команду BookSeatCommand у чергу.
  2. Booking Service (Consumer 1): Читає BookSeatCommand. Бронює місце в БД. Якщо успішно -> публікує подію SeatBookedEvent.
  3. Payment Service (Consumer 2): Підписаний на SeatBookedEvent. Бачить подію -> списує гроші. Якщо успішно -> публікує PaymentSucceededEvent.
  4. Notification Service (Consumer 3): Підписаний на PaymentSucceededEvent. Бачить подію -> відправляє PDF квиток на пошту.

Чому це круто? Ми можемо додати Analytics Service, який теж слухатиме PaymentSucceededEvent, щоб рахувати прибуток. І нам не треба змінювати код попередніх сервісів! Ми просто "підключаємо" нового слухача.


4. 🛠 Практична частина

Час забруднити руки. Уяви, що ти архітектор.

🔹 Завдання 1: Знайди Producer і Consumer

У додатку таксі (Uber/Uklon): 1. Ти натискаєш "Викликати таксі". 2. Водіям приходить сповіщення "Нове замовлення поруч". Хто тут Producer повідомлення, а хто Consumer? Що є повідомленням?

🔹 Завдання 2: "А що, якщо..."

Ти будуєш систему завантаження відео (як YouTube). Користувач завантажує файл 2GB. Потім нам треба: стиснути відео, створити прев'ю (thumbnail), повідомити підписників. Опиши ланцюжок повідомлень. Де тут синхронна дія, а де асинхронна?

🔹 Завдання 3: Міні-кейс (Рішення проблеми)

Система обробки замовлень піци. Message Broker (черга) зламався і втратив усі повідомлення за останні 5 хвилин. Що станеться з замовленнями, які касир (Producer) вже відправив, а кухня (Consumer) ще не отримала? Як би ти це виправив? (Підказка: чи зберігаємо ми десь дані перед відправкою? Pattern: Outbox).

🔹 Завдання 4: Refactoring

Перепиши логіку словами: Коли користувач змінює пароль, ми повинні розлогінити його на всіх інших девайсах. Зроби це через Event ("PasswordChanged").


5. 💡 Мислення як у розробника

Ось що відрізняє джуніора від сеньйора в цій темі.

Типова помилка новачка: Думати про повідомлення як про магію швидкості. "О, я запхну все в чергу, і все буде літати!" Але ти забуваєш, що тепер ти втратив узгодженість. Користувач оновив профіль, перезавантажив сторінку... а там старі дані. Чому? Бо Consumer ще не встиг оновити базу. Це називається Eventual Consistency (узгодженість у кінцевому рахунку).

Як думає профі: 1. Ідемпотентність (Idempotency): Що буде, якщо Consumer отримає одне й те саме повідомлення "Зніми $10" двічі? (Мережа глюкнула). Хороший розробник пише код так, щоб другий раз гроші не знялися (перевіряє ID повідомлення). 2. Dead Letter Queue (DLQ): Куди діваються повідомлення, які не вдалося обробити 5 разів підряд? (Наприклад, кривий JSON). Вони не повинні блокувати чергу. Вони йдуть у спеціальний відстійник (DLQ) для ручного розбору. 3. Tracing: Коли у тебе 50 сервісів перекидаються м'ячем, як знайти помилку? Потрібен Correlation ID — унікальний номер, який подорожує разом з повідомленням через усі сервіси.


6. 🧩 Підсумок

Отже, що ми маємо в сухому залишку:

  1. Message-driven архітектура — це як робота ресторану по чеках (тікетах).
  2. Вона дозволяє системі не зависати, якщо якась частина (кухня/пошта) працює повільно або впала.
  3. Producer кричить (публікує), Consumer слухає (обробляє), Broker передає.
  4. Ми жертвуємо миттєвим результатом заради надійності та масштабованості.

Що ти тепер вмієш? Ти вже не будеш пхати відправку емейлів, генерацію звітів і складні обчислення прямо в контролер відповіді користувачу. Ти знаєш, що такі задачі треба "відкласти в чергу".

🔜 Тизер наступного уроку: Але стривай... Якщо у нас 10 різних сервісів і кожен має свою маленьку базу даних, як нам зробити складний пошук по всіх даних одразу? На наступному уроці ми поговоримо про CQRS (Command Query Responsibility Segregation) — як розділити запис і читання, щоб це літало!

А поки — спробуй подумки перетворити свій ранок на Message-driven систему. Хто Producer твого сніданку? 😉

// CS50 style ending
print("Це був Message-driven урок. Побачимось!")