Представьте: интернет-магазин с 10 000 заказов в день. Клиент отменяет заказ, менеджер меняет статус, система теряет историю. Восстановить цепочку действий невозможно — в базе только текущее состояние. Без Event Sourcing аудит требует костылей: логирование, триггеры, дополнительные таблицы. Один наш клиент из финтеха потратил два месяца на расследование инцидента, потому что не было истории. После внедрения Event Sourcing мы сократили время аудита на 80%, что сэкономило клиенту более $50 000 в год. Наша команда имеет 5+ лет опыта внедрения Event Sourcing, реализовала более 10 проектов. Event Sourcing — ключевой паттерн событийно-ориентированной архитектуры, при котором каждое изменение состояния приложения фиксируется как неизменяемое событие. Текущее состояние восстанавливается повторным применением всех событий. Event Sourcing — шаблон проектирования, хранящий последовательность событий.
Архитектура Event Sourcing: Event Store, Aggregates и Replay
Event Store — основа
Основная таблица — append-only. Никакого UPDATE и DELETE:
CREATE TABLE event_store ( id BIGSERIAL PRIMARY KEY, event_id UUID UNIQUE NOT NULL, aggregate_id UUID NOT NULL, aggregate_type VARCHAR(100) NOT NULL, event_type VARCHAR(100) NOT NULL, event_version INT NOT NULL DEFAULT 1, payload JSONB NOT NULL, metadata JSONB NOT NULL DEFAULT '{}', occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), sequence_nr BIGINT NOT NULL -- глобальный порядок ); CREATE INDEX idx_es_aggregate ON event_store (aggregate_id, aggregate_type, id); CREATE INDEX idx_es_sequence ON event_store (sequence_nr); Оптимистичная блокировка — проверка sequence_nr перед записью нового события предотвращает конфликты конкурентных записей. Event Sourcing с PostgreSQL в 10 раз дешевле специализированных решений, но обеспечивает достаточную производительность для 95% проектов.
Aggregates: бизнес-логика на событиях
class OrderAggregate { private state: OrderState = { status: 'new', items: [], total: 0 }; private version = 0; private uncommittedEvents: DomainEvent[] = []; static rehydrate(events: DomainEvent[]): OrderAggregate { const order = new OrderAggregate(); for (const event of events) { order.apply(event); } return order; } placeOrder(items: OrderItem[]) { if (this.state.status !== 'new') throw new Error('Order already placed'); this.raise({ eventType: 'OrderPlaced', payload: { items, placedAt: new Date() } }); } private apply(event: DomainEvent) { switch (event.eventType) { case 'OrderPlaced': this.state.status = 'placed'; this.state.items = event.payload.items; break; case 'PaymentProcessed': this.state.status = 'paid'; this.state.paidAmount = event.payload.amount; break; case 'OrderShipped': this.state.status = 'shipped'; this.state.trackingNumber = event.payload.trackingNumber; break; } this.version++; } } Replay и снапшоты
При большом числе событий на агрегат (более 500) полный replay становится медленным. Снапшот — сериализованное состояние на момент N-го события. При загрузке читается последний снапшот + события после него.
Как снапшоты улучшают производительность?
async loadAggregate(aggregateId: string): Promise<OrderAggregate> { const snapshot = await this.snapshotRepo.findLatest(aggregateId); const fromSequence = snapshot?.version ?? 0; const events = await this.eventStore.getEvents( aggregateId, { fromVersion: fromSequence } ); const aggregate = snapshot ? OrderAggregate.fromSnapshot(snapshot) : new OrderAggregate(); return aggregate.rehydrate(events); } Снапшоты создаются асинхронно каждые 100–500 событий на агрегат. Оптимистичная блокировка при записи нового события проверяет sequence_nr — если он изменился, запись отклоняется, гарантируя целостность.
Временна́я сложность операций:
| Операция | Без снапшотов | Со снапшотами |
|---|---|---|
| Загрузка агрегата (N событий) | O(N) | O(recent events) |
| Запись события | O(1) | O(1) |
| Запрос по состоянию | O(N) projection rebuild | O(1) read model |
Проекции и эволюция схем
Проекции (Read Models) для быстрого чтения
Event Sourcing диктует разделение Write Model (события) и Read Model (проекции для запросов). Это типичная реализация CQRS. Проекция подписывается на поток событий и строит денормализованную таблицу:
class OrderProjection { async on(event: DomainEvent) { switch (event.eventType) { case 'OrderPlaced': await db.query(` INSERT INTO orders_view (id, status, customer_id, total, created_at) VALUES ($1, 'placed', $2, $3, $4) `, [event.aggregateId, event.payload.customerId, event.payload.total, event.occurredAt]); break; case 'OrderShipped': await db.query(` UPDATE orders_view SET status = 'shipped', tracking_number = $2, shipped_at = $3 WHERE id = $1 `, [event.aggregateId, event.payload.trackingNumber, event.occurredAt]); break; } } } Проекции можно удалить и пересобрать с нуля — история событий полная.
Schema Evolution: как не сломать историю
Версионирование схем событий — обязательная практика. Стратегии:
- Upcasting — при чтении старого события трансформировать его к новой схеме.
- Weak schema — JSON позволяет добавлять поля без поломки.
- Event versioning — хранить
eventVersion, читать с разными хендлерами.
Инструменты для Event Store
Готовые решения:
- EventStoreDB — специализированная СУБД с подписками и проекциями.
- Marten (.NET) — PostgreSQL как Event Store + документная БД.
- Axon Framework (Java) — полный ES/CQRS фреймворк.
Самописный на PostgreSQL достаточен для большинства проектов. LISTEN/NOTIFY для уведомления проекций о новых событиях. Брокер событий (Kafka, RabbitMQ, NATS JetStream) для распределения между сервисами.
Сравнение реализации Event Store:
| Решение | Производительность | Готовность | Сложность |
|---|---|---|---|
| PostgreSQL самописный | ~10 000 записей/с | Низкая (нужна разработка) | Средняя |
| EventStoreDB | ~100 000 записей/с | Высокая (коробка) | Низкая |
| Marten | ~5 000 записей/с | Средняя (есть .NET) | Средняя |
План внедрения и сроки
- Анализ домена и выделение агрегатов (1–2 недели).
- Определение типов событий и их схем (3–5 дней).
- Реализация Event Store (PostgreSQL или готовое решение) — 2–3 дня.
- Написание агрегатов с бизнес-логикой (3–5 дней на агрегат).
- Построение проекций для read models (5–7 дней).
- Настройка снапшотов для производительности (1–2 дня).
- Интеграция с брокером сообщений (Kafka, RabbitMQ) — 3–5 дней.
- Тестирование и мониторинг (1–2 недели).
Сроки: базовый Event Store на PostgreSQL — 2–3 дня. Один агрегат — 3–5 дней. Проекции + подписки + снапшоты — ещё 5–7 дней. Полная система с несколькими агрегатами, schema evolution и мониторингом — 3–5 недель.
Что входит в работу
Мы предоставляем:
- Документация по событиям и схемам.
- Код агрегатов, проекций и снапшотов.
- Настройка Event Store (PostgreSQL или EventStoreDB).
- Интеграция с брокером сообщений.
- Обучение команды работе с Event Sourcing.
- Поддержка после внедрения (1 месяц).
Почему стоит выбрать Event Sourcing?
- Полная история изменений для аудита.
- Возможность отката к любому состоянию.
- Разделение write и read моделей (CQRS).
- Простота добавления новых проекций без миграций.
Получите консультацию — мы проанализируем ваш домен и предложим оптимальное решение. Свяжитесь с нами для оценки вашего проекта. Мы поможем внедрить Event Sourcing с учётом ваших требований.







