Eventium PostgreSQL
PostgreSQL-based event store implementation for production event sourcing systems.
Overview
eventium-postgresql provides a robust, production-ready event store implementation backed by PostgreSQL. It leverages PostgreSQL's ACID guarantees, indexing capabilities, and reliability for persistent event storage with high performance and data integrity.
Features
- ✅ ACID Transactions - Full consistency guarantees
- ✅ Optimistic Concurrency - Prevents lost updates with version checks
- ✅ Efficient Indexing - Fast event retrieval by stream and position
- ✅ Global Event Ordering - Sequence numbers for time-ordered queries
- ✅ Type-Safe Access - Uses Persistent library for database operations
- ✅ Production Ready - Battle-tested PostgreSQL backend
- ✅ Multi-Process Support - Concurrent access from multiple applications
Database Schema
The implementation creates two main tables:
- Events Table - Stores events with aggregate keys, versions, and payloads
- Global Events Table - Maintains global ordering with sequence numbers
Indexes ensure fast lookups by:
- Stream key + version
- Global sequence number
- Event types (for projections)
Installation
Add to your package.yaml:
dependencies:
- eventium-core
- eventium-sql-common
- eventium-postgresql
- persistent-postgresql # PostgreSQL driver
Usage
import Eventium.Store.Postgresql
import Database.Persist.Postgresql
main :: IO ()
main = do
let connStr = "host=localhost dbname=eventstore user=postgres"
withPostgresqlPool connStr 10 $ \pool -> do
-- Initialize schema
flip runSqlPool pool $ do
runMigration migrateAll
-- Create event store
let store = makePostgresqlEventStore pool
-- Use with command handlers
result <- applyCommandHandler
(eventStoreWriter store)
(eventStoreReader store)
commandHandler
aggregateId
command
Configuration
Connection String
-- Basic connection
"host=localhost port=5432 dbname=mydb user=myuser password=mypass"
-- With connection pool
withPostgresqlPool connectionString poolSize $ \pool -> ...
Connection Pooling
Recommended settings for production:
-- Pool size based on concurrent requests
poolSize = numCores * 2 + effectiveSpindleCount
-- Example: 10 connections for typical web app
withPostgresqlPool connStr 10 $ \pool -> ...
Setup
Start PostgreSQL with Docker
# Using docker-compose (provided in project root)
docker-compose up -d postgres
# Or manually
docker run -d \
--name eventium-postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=eventstore \
-p 5432:5432 \
postgres:15
Run Migrations
runSqlPool (runMigration migrateAll) pool
PostgreSQL provides excellent performance characteristics:
- Writes: ~1000-5000 events/sec (single connection)
- Reads: ~10000-50000 events/sec (with proper indexing)
- Scalability: Read replicas for query scaling
See postgres-event-store-bench/ for benchmarking scripts.
Best Practices
- Use Connection Pooling - Essential for web applications
- Index Strategy - Default indexes cover common queries
- Backup Strategy - Regular PostgreSQL backups
- Monitoring - Watch connection pool usage and query performance
- Read Replicas - Scale read models with PostgreSQL replication
Production Considerations
- High Availability - Use PostgreSQL replication
- Backup & Recovery - Point-in-time recovery with WAL archiving
- Monitoring - Track event growth and query performance
- Connection Limits - Configure max_connections appropriately
Example: Complete Setup
import Eventium.Store.Postgresql
import Control.Monad.Logger (runStdoutLoggingT)
setupEventStore :: IO ()
setupEventStore = runStdoutLoggingT $ do
let connStr = "host=localhost dbname=eventstore"
withPostgresqlPool connStr 10 $ \pool -> do
-- Run migrations
flip runSqlPool pool $ runMigration migrateAll
-- Event store is ready to use
liftIO $ putStrLn "Event store initialized"
Documentation
License
MIT - see LICENSE.md