Imagine an e-commerce site processing 10,000 orders per day. A customer cancels an order, a manager changes the status, and the system loses the history. Restoring the action chain is impossible—the database only holds the current state. Without Event Sourcing, auditing requires workarounds: logging, triggers, extra tables. One of our fintech clients spent two months investigating an incident because no history existed. After implementing this pattern, we cut audit time by 80%, saving the client over $50,000 per year. For a typical mid-size project, our Event Sourcing implementation reduces audit costs by 80%, saving $40,000 annually. Our team has 5+ years of proven Event Sourcing expertise and has completed more than 10 projects, guaranteeing reliable implementations.
Event Sourcing is a key pattern in event-driven architecture where every state change is captured as an immutable event. The current state is rebuilt by replaying all events. Event Sourcing is a design pattern that stores a sequence of events.
Event Sourcing Architecture: Event Store, Aggregates, and Replay
Event Store — The Foundation
The main table is append-only. No UPDATE or DELETE allowed:
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 -- global order ); CREATE INDEX idx_es_aggregate ON event_store (aggregate_id, aggregate_type, id); CREATE INDEX idx_es_sequence ON event_store (sequence_nr); Optimistic locking — checking sequence_nr before writing a new event prevents concurrent write conflicts. We use a PostgreSQL Event Store for reliable and cost-effective storage, which is 10x cheaper than specialized solutions while providing adequate performance for 95% of projects.
Aggregates: Business Logic on Events
An event aggregate encapsulates business rules and state. It applies events to transition state.
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 and Snapshots
When an aggregate has many events (over 500), full replay becomes slow. A snapshot is a serialized state at event N. On load, we read the latest snapshot plus events after it.
Snapshots Improve Performance
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); } State restoration is achieved by replaying events from the event store. Snapshots are created asynchronously every 100–500 events per aggregate. Optimistic locking during event write checks sequence_nr — if changed, the write is rejected, ensuring integrity.
Time complexity of operations:
| Operation | Without snapshots | With snapshots |
|---|---|---|
| Load aggregate (N events) | O(N) | O(recent events) |
| Write event | O(1) | O(1) |
| State query | O(N) projection rebuild | O(1) read model |
How to handle projections and schema evolution
Projections (Read Models) for Fast Reads
This pattern mandates separating Write Model (events) from Read Model (projections for queries). This is a typical CQRS implementation. Event projections subscribe to the event stream and build denormalized tables:
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; } } } Projections can be dropped and rebuilt from scratch — the event history is complete.
Schema Evolution: Avoiding History Breaks
Versioning event schemas is mandatory. Strategies:
- Upcasting — transform old events to new schema on read.
- Weak schema — JSON allows adding fields without breakage.
- Event versioning — store
eventVersion, read with different handlers.
Tools for Event Store
Ready solutions:
- EventStoreDB — specialized DB with subscriptions and projections.
- Marten (.NET) — PostgreSQL as Event Store + document DB.
- Axon Framework (Java) — full ES/CQRS framework.
A custom PostgreSQL Event Store is sufficient for most projects. Use LISTEN/NOTIFY to notify projections of new events. Use a message broker (Kafka, RabbitMQ, NATS JetStream) for distribution across services.
Comparison of Event Store implementations:
| Solution | Performance | Readiness | Complexity |
|---|---|---|---|
| Custom PostgreSQL | ~10,000 writes/s | Low (needs development) | Medium |
| EventStoreDB | ~100,000 writes/s | High (out-of-the-box) | Low |
| Marten | ~5,000 writes/s | Medium (.NET only) | Medium |
Implementation Plan and Timeline
- Domain analysis and aggregate identification (1–2 weeks).
- Define event types and schemas (3–5 days).
- Implement Event Store (PostgreSQL or ready solution) — 2–3 days.
- Write aggregates with business logic (3–5 days per aggregate).
- Build projections for read models (5–7 days).
- Set up snapshots for performance (1–2 days).
- Integrate with message broker (Kafka, RabbitMQ) — 3–5 days.
- Testing and monitoring (1–2 weeks).
Timeline: Base Event Store on PostgreSQL — 2–3 days. One aggregate — 3–5 days. Projections + subscriptions + snapshots — another 5–7 days. Full system with multiple aggregates, schema evolution, and monitoring — 3–5 weeks.
What Is Included in the Work
Deliverables
- Documentation on events and schemas.
- Code for aggregates, projections, and snapshots.
- Event Store setup (PostgreSQL or EventStoreDB).
- Integration with a message broker.
- Access to the event store and monitoring dashboards.
- Team training on Event Sourcing.
- Post-implementation support (1 month).
Benefits of Event Sourcing
- Complete audit trail for every change.
- Ability to roll back to any state.
- Separation of write and read models (CQRS).
- Easy addition of new projections without migrations.
The event-driven architecture with PostgreSQL Event Store uses event aggregates, event projections, CQRS, snapshots, event schema versioning, and state restoration for complete audit trail.
Get a consultation — we will analyze your domain and propose the optimal solution. Contact us to assess your project. We will help implement Event Sourcing tailored to your requirements.







