Skip to content

Core Concepts

The fundamentals of event sourcing with DeltaBase

Here is a simple test list:

  • First item
  • Second item
  • Third item

And here’s a numbered list:

  1. First numbered item
  2. Second numbered item
  3. Third numbered item

Event sourcing sounds complex, but it’s just a different way of storing data. Instead of saving the current state, you save the events that led to that state.

Think of it like a bank statement. Your balance isn’t stored anywhere - it’s calculated from all the deposits and withdrawals. That’s event sourcing.

Events are facts. Things that happened. They’re immutable and always past tense.

// Good events - past tense, factual
{
type: 'order.placed',
data: { customerId: 'abc', total: 99.99, items: [...] }
}
{
type: 'payment.authorized',
data: { orderId: '123', amount: 99.99, cardLast4: '1234' }
}
// Bad events - present tense, commands
{
type: 'place.order', // This is a command, not an event
data: { ... }
}

Event structure in DeltaBase:

  • type - What happened (like ‘user.registered’)
  • data - The details of what happened
  • metadata - Optional context (who, why, etc.)

DeltaBase adds:

  • streamId - Which entity this relates to
  • streamPosition - Order within the stream
  • globalPosition - Order across all events
  • eventId - Unique identifier
  • createdAt - When it happened

Streams are sequences of related events. Usually one per entity.

// Stream: user-123
[
{ type: 'user.registered', data: { email: 'john@example.com' } },
{ type: 'user.email_verified', data: { verifiedAt: '...' } },
{ type: 'user.profile_updated', data: { name: 'John Doe' } }
]
// Stream: order-456
[
{ type: 'order.placed', data: { customerId: 'user-123', total: 99.99 } },
{ type: 'order.paid', data: { paymentId: 'pay_abc' } },
{ type: 'order.shipped', data: { trackingNumber: 'TRK123' } }
]

Stream naming conventions:

  • user-{id} for user events
  • order-{id} for order events
  • session-{id} for session events
  • Whatever makes sense for your domain

Current state is calculated by replaying events:

function buildUserFromEvents(events) {
let user = { id: null, email: null, verified: false, name: null };
for (const event of events) {
switch (event.type) {
case 'user.registered':
user.id = event.streamId;
user.email = event.data.email;
break;
case 'user.email_verified':
user.verified = true;
break;
case 'user.profile_updated':
if (event.data.name) user.name = event.data.name;
if (event.data.email) user.email = event.data.email;
break;
}
}
return user;
}

This is called aggregation - building current state from events.

Commands change state by creating events:

// Command: Register a user
async function registerUser(email, name) {
const userId = generateId();
await eventStore.appendToStream(userId, [
{
type: 'user.registered',
data: { email, name }
}
]);
return userId;
}

Queries read current state:

// Query: Get user profile
async function getUserProfile(userId) {
const events = await eventStore.readStream(userId);
return buildUserFromEvents(events);
}

Why separate them?

  • Commands can be optimized for writes
  • Queries can be optimized for reads
  • You can have multiple views of the same data
  • Scales better under load

Multiple people editing the same data? DeltaBase handles it with version numbers.

// Load current version
const events = await eventStore.readStream('order-123');
const currentVersion = events.length - 1;
// Try to append, expecting specific version
try {
await eventStore.appendToStream('order-123', [updateEvent], {
expectedStreamVersion: currentVersion
});
} catch (error) {
if (error.status === 409) {
// Someone else modified it - handle the conflict
console.log('Order was modified by someone else');
}
}

This prevents the “lost update” problem where one person’s changes overwrite another’s.

Traditional database:

UPDATE users SET email = 'new@example.com' WHERE id = 123;
-- Old email is gone forever

Event store:

await eventStore.appendToStream('user-123', [
{
type: 'user.email_changed',
data: {
oldEmail: 'old@example.com',
newEmail: 'new@example.com'
}
}
]);
// Both emails are preserved in the audit trail

Key differences:

  • Immutable: Events never change, only accumulate
  • Auditable: Complete history of what happened
  • Temporal: Can query any point in time
  • Append-only: Optimized for writes

Sometimes you need optimized read models. Build them from events:

// Projection: User summary view
async function updateUserSummaryView(event) {
switch (event.type) {
case 'user.registered':
await db.userSummaries.create({
userId: event.streamId,
email: event.data.email,
status: 'registered',
createdAt: event.createdAt
});
break;
case 'user.email_verified':
await db.userSummaries.update(event.streamId, {
status: 'verified'
});
break;
}
}

Views vs Events:

  • Events: The source of truth
  • Views: Optimized for queries
  • Eventually consistent: Views might lag behind events

DeltaBase can notify your app when events happen:

// Subscribe to user events
await eventBus.subscribeWebhook('user.*', 'https://myapp.com/webhook');
// Your webhook receives:
{
"event": {
"type": "user.registered",
"data": { "email": "john@example.com" },
"streamId": "user-123",
"createdAt": "2024-01-01T12:00:00Z"
}
}

Perfect for updating UI in real-time or triggering workflows.

Traditional thinking: “What is the current state?” Event sourcing thinking: “What happened to get to this state?”

Traditional: User has email “john@example.comEvent sourcing: User registered with “john@example.com”, then changed it from “old@example.com” to “john@example.com

The difference is profound:

  • You know why the state is what it is
  • You can prove what happened
  • You can replay scenarios to debug issues
  • You can time travel to any point in history

Event sourcing feels weird at first. You’re not updating records, you’re appending facts. But once it clicks, you realize it’s actually simpler:

  • No complex UPDATE statements
  • No lost data from overwrites
  • No mysterious state changes
  • No missing audit trails

Just facts, in order, forever.

That’s event sourcing. That’s DeltaBase.