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.







