Event sourcing требует от вас перестать перезаписывать данные. В традиционном CRUD-приложении обновление адреса доставки пользователя означает поиск строки, изменение значения и отбрасывание предыдущего состояния. Event sourcing идет другим путем. Он сохраняет каждое изменение как неизменяемый факт: пользователь создал аккаунт, обновил адрес, подтвердил email. Текущее состояние системы не хранится напрямую. Оно вычисляется путем последовательного воспроизведения этих событий.
Этот паттерн решает реальные задачи. Журналы аудита становятся бесплатным побочным продуктом. Вы можете восстановить состояние заказа в любой момент в прошлом. Вы можете проводить отладку, воспроизводя именно то, что произошло. Обратная сторона — сложность. Теперь вместо простых строк вы управляете потоками фактов, моделями чтения и согласованностью в конечном счете (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_id, version) предотвращает ситуацию, когда два пишущих процесса пытаются добавить один и тот же порядковый номер. Когда поступает команда, вы считываете текущую версию этого потока, увеличиваете её и вставляете новое событие внутри транзакции. Если другой процесс успел сделать это раньше вас, сработает ограничение уникальности, и вам придется повторить попытку или отклонить команду.
Рассмотрим конкретный пример. Вы управляете системой учета запасов. Вместо одной строки inventory со столбцом quantity, вы добавляете события в таблицу inventory_events. ItemReceived добавляет десять единиц. ItemReserved списывает две. ItemShipped списывает три. Чтобы узнать текущий остаток для SKU-42, вы суммируете полезные нагрузки соответствующих событий. Чтобы узнать остаток три дня назад, вы суммируете данные только до этой временной метки. Если ошибка в логике отгрузки привела к сбою в прошлый вторник, вы можете воспроизвести события через исправленный код, чтобы получить истинное состояние. Это невозможно сделать с помощью обычного оператора UPDATE.
Принципы, которые уберегут вас от проблем
Использование Postgres не отменяет необходимости в дисциплине. Следующие принципы применимы непосредственно к системам на базе event sourcing.
Будьте проще. Сложность убивает надежность. Сопротивляйтесь желанию построить универсальный фреймворк событий, прежде чем запустите хотя бы один рабочий процесс. Одной таблицы, функции репозитория для добавления событий и воркера проекций для построения моделей чтения достаточно, чтобы доказать ценность подхода. Добавляйте инструменты только тогда, когда возникнет конкретная проблема.
Начинайте с малого. Не переписывайте весь свой монолит. Выберите один ограниченный контекст (bounded context), где наличие журнала аудита оправдает накладные расходы. Хорошими кандидатами будут бухгалтерская книга, движок рабочих процессов или система резервирования запасов. Постройте этот конвейер от начала до конца. Запустите его в продакшене. А затем решите, стоит ли расширяться.
Сначала определите критерии успеха. Event sourcing — это не архитектура «по умолчанию», а решение конкретных задач. Если ваша задача — только отслеживать последнее состояние, CRUD будет быстрее и дешевле. Если вам нужны временные запросы, строгая проверяемость (auditability) или возможность перестраивать модели чтения по запросу, тогда использование событий имеет смысл. Прежде чем внедрять это, поймите, какую именно проблему вы решаете.
Сначала измеряйте, потом оптимизируйте. Современный PostgreSQL на скромном оборудовании может принимать тысячи событий в секунду, используя простую таблицу только для добавления (append-only). Не шардируйте свое хранилище событий и не внедряйте сложные схемы партиционирования, пока мониторинг не докажет, что вы исчерпали более простые способы решения. Индексируйте поля, по которым делаете запросы. Настройте autovacuum для рабочих нагрузок типа append-only. А затем измерьте показатели снова.
Test everything. Unit test your event handlers. Integration test the append path. Most importantly, test failure scenarios. What happens when two nodes append to the same stream simultaneously? What happens when a projection worker crashes mid-batch? Write tests that verify your optimistic concurrency and your at-least-once delivery guarantees.
Monitor in production. The events table will grow. Unlike a normalized schema where updates keep row counts flat, event sourcing is intentionally additive. Track table size, disk I/O, and the lag between your write model and your read model projections. Set alerts on projection lag before your users notice stale data.
Automate manual tasks. Manual schema changes, manual projection rebuilds, and manual event replay are ticking time bombs. Script your migration strategy. If you evolve an event schema, automate the upcasting or transformation so that old events can be replayed through new logic without human intervention at midnight.
Document your choices. Write down why specific streams exist, what each event type means, and when the team should choose CRUD over events. Event sourcing introduces cognitive load. Good documentation prevents a new engineer from guessing wrong and appending malformed events to a critical stream.
Traps That Waste Months
Event sourcing has a way of sounding elegant in a diagram and painful in production. Watch for these traps.
Underestimating complexity. Replaying events to rebuild state is conceptually simple. Managing idempotency, snapshotting for performance, and compensating transactions across aggregates is not. Break your system into small pieces. Solve one stream at a time.
Over-engineering. Do not provision a multi-node Kafka cluster because you imagine your event volume will one day require it. Postgres can carry you surprisingly far. Introduce new infrastructure only when you have a measured bottleneck that you cannot fix within your current setup.
Ignoring technical debt. Old event schemas linger forever. If you change your OrderCreated payload, you still have ten million historical events in the old shape. Track this debt. Plan backward-compatible readers or migration scripts. Do not let the burden of legacy events slow every new feature.
Choosing tools the team cannot run. The best architecture fails if only one person understands it. If your team knows Postgres and SQL, start there. If you introduce a specialized event store, make sure you have the operational expertise to debug it at two in the morning.
A Practical Starting Point
If this approach fits your problem, do not wait for a five-quarter rewrite. Start this week.
Audit your current systems. Find a place where an audit trail would solve real pain. Maybe it is an order state machine that currently maintains a single status column. Perhaps it is a financial ledger where balance corrections require manual database patches. Pick one gap where overwriting state has hurt you.
Then pick one small improvement you can make today. Create one event table. Model one stream. Write one projection that builds a read model from those events. Deploy it behind a feature flag. Watch it handle real traffic.
Event sourcing with PostgreSQL is not magic. It is a practical tool for teams that need to know not just where things are, but how they got there. Build slowly, measure honestly, and let your actual requirements guide the architecture.
