Core Concepts
Understanding Ventyd's building blocks
What is Event Sourcing?
Ventyd uses Event Sourcing - a pattern where you store all changes as events instead of just the current state. Instead of updating database rows, you store every change as an immutable event:
// Traditional approach: UPDATE users SET name = 'Alice' WHERE id = 1
// Event sourcing approach:
{
eventName: "user:name_updated",
body: { name: "Alice" },
timestamp: "2024-01-15T10:30:00Z"
}Benefits:
- Complete audit trail
- Time travel (replay events to any point)
- Easy debugging (see exactly what happened)
- No data loss (every change is recorded)
The Four Building Blocks
Events
Immutable facts that happened
{
eventId: "evt_123",
eventName: "user:created",
eventCreatedAt: "2024-01-15T10:30:00Z",
body: { email: "alice@example.com" }
}Reducer
Pure function that builds state from events
const reducer = (prevState, event) => {
switch (event.eventName) {
case "user:created":
return { email: event.body.email };
case "user:name_updated":
return { ...prevState, name: event.body.name };
default:
return prevState;
}
};Entity
Domain object with business logic
class User extends Entity(schema, reducer) {
get email() {
return this.state.email;
}
updateName = mutation(this, (dispatch, name: string) => {
if (!name) throw new Error("Name required");
dispatch("user:name_updated", { name });
});
}Repository
Saves and loads entities
const userRepository = createRepository(User, { adapter });
// Create and save
const user = User.create({ body: { email: "alice@example.com" } });
await userRepository.commit(user);
// Load later
const loaded = await userRepository.findOne({ entityId: user.entityId });How It Works
1. User calls mutation → 2. Mutation dispatches event
↓
4. Reducer builds state ← 3. Event is validated
↓
5. Repository saves event to databaseWhen you load an entity, Ventyd:
- Loads all events from database
- Replays them through the reducer
- Builds the current state
Example Flow
Let's see how all the pieces work together in a complete example:
// Create user
const user = User.create({
body: { email: "alice@example.com", name: "Alice" }
});
// Update name
user.updateName("Alice Cooper");
// Save to database
await userRepository.commit(user);
// Later... load from database
const loaded = await userRepository.findOne({ entityId: user.entityId });
console.log(loaded.name); // "Alice Cooper"
// See all events
console.log(loaded.events);
// [
// { eventName: "user:created", body: { email: "...", name: "Alice" } },
// { eventName: "user:name_updated", body: { name: "Alice Cooper" } }
// ]Key Principles
Events are Immutable
Once created, events never change. This enables time travel and reliable audit trails.
Reducers are Pure
Same events always produce same state. No side effects, no randomness, no API calls.
Mutations Validate First
Always validate before dispatching events. Never dispatch invalid events.
State is Derived
State is computed from events. You can always rebuild it by replaying events.
If state is always derived from events, what happens when an entity has thousands of events? And how do you query entities by fields other than entityId? These questions lead to two additional concepts — snapshots and views — that build on top of the event log. See Events, Snapshots & Views for the full picture.
