Event sourcing вимагає від вас припинити перезаписувати дані. У традиційному CRUD-додатку оновлення адреси доставки користувача означає пошук рядка, зміну значення та відкидання попереднього стану. Event sourcing обирає інший шлях. Він зберігає кожну зміну як незмінний факт: користувач створив акаунт, оновив адресу, підтвердив email. Поточний стан системи не зберігається безпосередньо. Він обчислюється шляхом послідовного відтворення цих подій.

Цей патерн вирішує реальні проблеми. Аудиторські журнали стають безкоштовним побічним продуктом. Ви можете реконструювати стан замовлення в будь-який момент у минулому. Ви можете налагоджувати систему, відтворюючи саме те, що сталося. Компромісом є складність. Тепер замість простих рядків ви керуєте потоками фактів, моделями читання (read models) та остаточною узгодженістю (eventual consistency).

PostgreSQL може виступати як ваше сховище подій (event store). Більшість команд уже використовують його. Він пропонує ACID-транзакції, JSONB для гнучких корисних навантажень та перевірені інструменти резервного копіювання. Вам не потрібно впроваджувати Kafka, Cassandra або спеціалізовану базу даних для сховища подій з першого дня. Стандартний екземпляр Postgres забезпечує транзакційні гарантії та аудиторські журнали, яких вимагає event sourcing, не розширюючи вашу інфраструктуру.

Структура сховища подій у Postgres

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

  • id як bigserial або UUID, що слугує для глобального впорядкування.
  • stream_id для групування пов'язаних подій, наприклад, усіх змін для одного користувача або замовлення.
  • event_type як звичайний текст: UserEmailChanged, PaymentReceived, InventoryAdjusted.
  • payload як JSONB, що містить конкретні дані для цієї події.
  • occurred_at з точністю до часового поясу.
  • version для кожного потоку (stream), що забезпечує оптимістичну конкурентність.

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

Розглянемо конкретний приклад. Ви керуєте системою інвентаризації. Замість одного рядка inventory зі стовпцем quantity, ви додаєте події до таблиці inventory_events. ItemReceived додає десять одиниць. ItemReserved видаляє дві. ItemShipped видаляє три. Щоб дізнатися поточний запас для SKU-42, ви підсумовуєте відповідні дані (payload) подій. Щоб дізнатися запас три дні тому, ви підсумовуєте події лише до відповідного часового штампа. Якщо помилка в логіці доставки сталася минулого вівторка, ви відтворюєте події через виправлений код, щоб отримати справжній стан. Ви не можете зробити це за допомогою простої команди UPDATE.

Принципи, які вбережуть вас від неприємностей

Побудова системи на Postgres не усуває потреби в дисципліні. Наступні принципи застосовуються безпосередньо до систем на основі event sourcing.

Будьте простими. Складність вбиває надійність. Спротивтеся спокусі створити універсальний фреймворк для подій до того, як ви запустите хоча б один робочий процес. Однієї таблиці, функції репозиторію для додавання подій та воркера проекцій (projection worker) для побудови моделей читання (read models) достатньо, щоб довести цінність рішення. Додавайте інструменти лише тоді, коли з'явиться конкретна проблема.

Починайте з малого. Не переписуйте весь свій моноліт. Оберіть один обмежений контекст (bounded context), де аудиторський журнал виправдає витрати на складність. Облікова книга платежів, механізм робочих процесів (workflow engine) або система резервування запасів — хороші кандидати. Побудуйте цей один конвеєр від початку до кінця. Запустіть його в продакшені. А вже потім вирішуйте, чи варто розширюватися.

Спочатку визначте успіх. Event sourcing — це не архітектура за замовчуванням; це рішення для конкретних потреб. Якщо ваша вимога полягає лише у відстеженні останнього стану, CRUD буде швидшим і дешевшим. Якщо вам потрібні часові запити (temporal queries), сувора можливість аудиту або можливість перебудувати моделі читання за запитом, тоді події мають сенс. Зрозумійте, яку проблему ви вирішуєте, перш ніж братися за справу.

Вимірюйте перед оптимізацією. Сучасний PostgreSQL на скромному обладнанні може приймати тисячі подій на секунду за допомогою простої таблиці, що працює тільки на додавання (append-only). Не використовуйте шардування вашого сховища подій і не впроваджуйте складні схеми партиціювання, доки моніторинг не доведе, що ви вичерпали простіші методи вирішення. Індексуйте поля, за якими робите запити. Налаштуйте autovacuum для навантажень типу append-only. А потім виміряйте знову.

Тестуйте все. Проводьте модульне тестування ваших обробників подій. Проводьте інтеграційне тестування шляху додавання (append path). Найголовніше — тестуйте сценарії відмов. Що станеться, якщо два вузли одночасно намагатимуться додати дані до одного й того самого потоку? Що станеться, якщо воркер проекції вийде з ладу посеред обробки пакету (batch)? Пишіть тести, які перевіряють вашу оптимістичну конкурентність та гарантії доставки «принаймні один раз» (at-least-once delivery).

Моніторте в продакшені. Таблиця подій буде зростати. На відміну від нормалізованої схеми, де оновлення не змінюють кількість рядків, event sourcing є навмисно аддитивним. Відстежуйте розмір таблиці, дискове введення/виведення (I/O) та затримку (lag) між вашою моделлю запису та проекціями моделі читання. Налаштуйте сповіщення про затримку проекції до того, як ваші користувачі помітять застарілі дані.

Автоматизуйте ручні завдання. Ручні зміни схеми, ручне перебудування проекцій та ручне відтворення подій (event replay) — це бомби сповільненої дії