Postgresql Event Sourcing
A reference implementation of an event-sourced system that uses PostgreSQL as an event store built with Spring Boot. Fork the repository and use it as a template for your projects. Or clone the repository and run end-to-end tests to see how everything works together.
Install / Use
npx skills add eugene-khyst/postgresql-event-sourcingInstalls into whichever agent you are using.
README
<a id="0"></a>Event Sourcing with PostgreSQL
- Introduction
- Example domain
- Event sourcing and CQRS basics
- Solution architecture
- Project structure
- How to adapt it to your domain?
- How to run the sample?
<a id="1"></a>Introduction
Usually, our applications operate with the current state of a domain object. But sometimes, we need to know the entire history of the domain object changes. For example, we want to know how an order got into its current state.
The audit trail (also called the audit log) is a chronological record of the history and details of the actions that affected the system. An audit trail may be a regulatory or business requirement.
We can store all changes to the domain object state as a sequence of events in an append-only event stream. Thus, event streams will contain an entire history of changes. But how can we be sure that this history is authentic and error-free? We can use event streams as a primary source of truth in a system. To get the current state of an object, we have to replay all events in the order of occurrence. This pattern is called event sourcing. The database for storing event streams is called an event store. Event sourcing provides a complete and accurate record of all changes made to a system. Event sourcing is an industry standard for implementing audit trail.
There are specialized databases for event sourcing. Developer Advocates working for the companies behind these specialized databases said you shouldn't implement event sourcing with traditional relational or document-oriented databases. Is this true or just a marketing ploy?
Specialized databases for event sourcing are convenient and provide the necessary functionality out of the box. But PostgreSQL, the world's most advanced open-source database, is also suitable for event sourcing. You can use PostgreSQL as an event store without additional frameworks or extensions instead of setting up and maintaining a separate specialized database for event sourcing.
This repository provides a reference implementation of an event-sourced system that uses PostgreSQL as an event store built with Spring Boot. Fork the repository and use it as a template for your projects. Or clone the repository and run end-to-end tests to see how everything works together.

See also
<a id="2"></a>Example domain
This sample uses a simplified domain model of the ride-hailing system.
- A rider can place an order for a ride along a route specifying a price.
- A rider can edit an order price to pay more instead of waiting in cases of very high demand.
- A driver can accept an order.
- A driver can complete previously accepted order.
- An order can be canceled before completion.
<a id="3"></a>Event sourcing and CQRS basics
<a id="3-1"></a>State-oriented persistence
State-oriented persistence (CRUD) applications store only the latest version of an entity.
Database records present entities.
When an entity is updated, the corresponding database record gets updated too.
SQL INSERT, UPDATE and DELETE statements are used.
<a id="3-2"></a>Event sourcing
Event sourcing applications persist the state of an entity as a sequence of immutable state-changing events.
Whenever the state of an entity changes, a new event is appended to the list of events.
Only SQL INSERT statements are used.
Events are immutables, so SQL UPDATE and DELETE statements are not used.
The current state of an entity can be restored by replaying all its events.
Event sourcing is closely related to domain-driven design (DDD) and shares some terminology.
An entity in event sourcing is called an aggregate.
A sequence of events for the same aggregate is called a stream.
Event sourcing is best suited for short-living entities with a small total number of events (e.g., orders).
Restoring the state of the short-living entity by replaying all its events doesn't have any performance impact. Thus, no optimizations for restoring state are required for short-living entities.
For endlessly stored entities (e.g., users, bank accounts) with thousands of events restoring state by replaying all events is not optimal, and snapshotting should be considered.
<a id="3-3"></a>Snapshotting
Snapshotting is an optimization technique where a snapshot of the aggregate's state is also saved, so an application can restore the current state of the aggregate from the snapshot rather than from all the events (potentially thousands).
On every nth event, make an aggregate snapshot by storing an aggregate state and its version.
To restore an aggregate state:
- first read the latest snapshot,
- then read events forward from the original stream starting from the version pointed by the snapshot.
<a id="3-4"></a>Querying the data
It's easy to find an aggregate by ID, but other queries are difficult. Since aggregates are stored as append-only lists of immutable events, querying the data using SQL, as we used to, is impossible. To find an aggregate by some field, we need to first read all the events and replay them to restore all the aggregates.
To bring back all the querying power a relational database has to offer, we can create a dedicated read model derived from the event stream.
The event stream is the write model and the primary source of truth.
The read model is a "denormalized" view of the write model, allowing faster and more convenient querying. Read models are projections of the system state. Therefore, read models are also known as projections.
Projections provide a view of data for a single aggregate type or perform aggregations and combine data from multiple aggregate types.
That's where CQRS comes in handy.
<a id="3-5"></a>CQRS
Command-query responsibility segregation (CQRS) stands for segregating the responsibility between commands (write requests) and queries (read requests). The write requests and the read requests are processed by different handlers.
A command generates zero or more events or results in an error.
CQRS is a self-sufficient architectural pattern and doesn't require event sourcing. But in practice, event sourcing is usually used in conjunction with CQRS. Event store is used as a write database, and SQL or NoSQL database as a read database.
<a id="3-6"></a>Event handlers
Commands generate events. Event processing is done by event handlers. As a part of event processing, we may need to update projections, send a message to a message broker, or make an API call.
There are two types of event handlers: synchronous and asynchronous.
Storing the write model and read model in the same database allows for transactional updates of the read model. Each time we append a new event, the projection is updated synchronously in the same transaction. The projection is consistent with the event stream.
When an event handler communicates with an external system or middleware (e.g., sends a message to Kafka), it should run asynchronously after the transaction updating the write model. Asynchronous execution leads to eventual consistency.
Communication with external systems should not occur in the same transaction updating the write model. The external system call may succeed, but the transaction will later be rolled back, resulting in an inconsistency.
Anyway, distributed systems should be designed with eventual consistency in mind.
<a id="3-7"></a>Domain events vs Integration events
Events in event sourcing are domain events. The domain event is a part of a bounded context and should not be used "as-is" for integration with other bounded contexts.
For communication between bounded contexts integration events are used. The integration event represents the current state of an aggregate, not just changes to the aggregate as a domain event.
<a id="3-8"></a>Advantages of CQRS
- Independent scaling of the read and write databases.
- Optimized data schema for the read database (e.g. the read databases can be denormalized).
- Simpler queries (e.g. complex
JOINo
Related Skills
codebase-memory-mcp
38.2kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
codebase-memory-mcp
38.2kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
codebase-memory-mcp
38.2kHigh-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.
tabularis
4.0kOpen-source desktop SQL workspace for PostgreSQL, MySQL/MariaDB, SQLite and 15+ more databases like DuckDB, ClickHouse, Redis and Firestore. Built-in MCP server for Claude, Cursor and Devin, SQL notebooks and visual EXPLAIN.
