Enterprise Patterns
Event Sourcing
Using events as the source of truth
Event sourcing is the pattern of storing state changes as a sequence of events rather than updating a single record. In PURISTA, every command can emit events, and subscriptions can project those events into read models. This makes event sourcing a natural fit.
Core idea
Instead of:
UPDATE users SET name = 'John' WHERE id = 123
Store:
Event: userNameChanged { userId: 123, oldName: 'Jane', newName: 'John', timestamp: ... }
The current state is derived by replaying all events for an entity.
PURISTA and event sourcing
PURISTA’s message model maps naturally to event sourcing:
| Event Sourcing Concept | PURISTA Equivalent |
|---|---|
| Command | PURISTA command |
| Event | Command success event |
| Event store | Not included — add your own (e.g., EventStoreDB, Kafka with compaction, or a Postgres events table) |
| Projection | Subscription that builds read models |
| Snapshot | State store cache of current state |
PURISTA routes events through the event bridge, but the bridge is not an event store — events are not persisted or replayable by default. To implement true event sourcing, you need to add a durable event store that your commands write to and your projections replay from.
Example: user profile
The command emits events
const updateProfileCommand = userServiceBuilder
.getCommandBuilder('updateProfile', 'Update user profile', 'profileUpdated')
.addPayloadSchema(z.object({ userId: z.string(), name: z.string() }))
.addOutputSchema(z.object({ userId: z.string(), name: z.string() }))
.setCommandFunction(async function (context, payload) {
const current = await context.states.getState(`profile:${payload.userId}`)
const oldName = current[`profile:${payload.userId}`]?.name
await context.states.setState(`profile:${payload.userId}`, {
userId: payload.userId,
name: payload.name,
})
// Emit the event
await context.emit('profileUpdated', {
userId: payload.userId,
oldName,
newName: payload.name,
timestamp: Date.now(),
})
return { userId: payload.userId, name: payload.name }
})
Subscriptions project read models
const profileProjection = analyticsServiceBuilder
.getSubscriptionBuilder('profileProjection', 'Build profile search index')
.subscribeToEvent('profileUpdated')
.setSubscriptionFunction(async function (context, payload) {
await context.resources.searchIndex.update({
userId: payload.userId,
name: payload.newName,
})
return { status: 'ack' }
})
When to use event sourcing
All of these use cases require a durable event store (not included in PURISTA — you must add one):
- Audit trails are required (finance, healthcare, compliance)
- You need to replay history to rebuild state
- Temporal queries are needed (“What was the state on March 1st?”)
- Multiple read models from the same events
- Event-driven architectures with complex business rules
When NOT to use event sourcing
- Simple CRUD with no audit requirements
- Small domains where replay overhead exceeds value
- Teams unfamiliar with eventual consistency
- When strict ACID transactions are required per operation
Common pitfalls
- Not versioning events. Event schemas evolve. Version them from the start.
- Missing snapshots. Replaying thousands of events is slow. Snapshots cache current state.
- Tight coupling to projections. Projections should be independent; they can be rebuilt.
- Ignoring eventual consistency. Projections are async; they may lag behind the event stream.
Checklist
- Events are versioned and schema-evolvable
- Snapshots prevent unbounded replay
- Projections are independent and rebuildable
- A durable event store is in place (EventStoreDB, Kafka with compaction, or a custom events table)
- Temporal queries are tested
- Projection lag is monitored