Event Streaming Architecture
Most databases store current state: the row holds the latest values for a record and past values are gone. An event log stores something different: every change that ever happened, in the order it happened, as an immutable sequence. Current state is derived from that history, not stored directly. This reversal has a quiet consequence that matters enormously for realtime systems: any client can replay the log from any position and arrive at the correct state, which makes catch-up, audit trails, undo, and time-travel debugging structural properties of the architecture rather than features you bolt on.
This guide connects the catch-up mechanics from realtime data synchronization to the underlying log model and provides the architectural vocabulary for the collaborative patterns in the next guides.
What you'll learn
The append-only event log
An append-only log accepts writes at only one end and never modifies or deletes past entries. That constraint is what makes replay possible.
Events as immutable facts
// Events are immutable facts, not mutable commands
var events = [
{ offset: 1, type: 'doc.created', payload: { id: 'abc', title: 'Draft' } },
{ offset: 2, type: 'doc.renamed', payload: { id: 'abc', title: 'Final' } },
{ offset: 3, type: 'doc.archived', payload: { id: 'abc' } },
{ offset: 4, type: 'doc.created', payload: { id: 'xyz', title: 'Notes' } }
];
// State is derived by reducing over events
function buildState(events) {
return events.reduce(function (state, event) {
switch (event.type) {
case 'doc.created':
state[event.payload.id] = { title: event.payload.title, archived: false };
break;
case 'doc.renamed':
if (state[event.payload.id]) state[event.payload.id].title = event.payload.title;
break;
case 'doc.archived':
if (state[event.payload.id]) state[event.payload.id].archived = true;
break;
}
return state;
}, {});
}
Past events never change. A mistake in the application is corrected by appending a compensating event, not by editing the wrong one. This means the log is a complete, auditable history: you can replay it from any offset and observe the state at any point in time. The reduce pattern above is the most direct expression of state reconstruction from an event stream.
Event sourcing and state reconstruction
Event sourcing takes the log idea further: the event log is the primary storage, not a derived view of it.
Snapshots for fast startup
// Rebuild from a recent snapshot + events since
async function loadState(entityId) {
var snapshot = await store.getLatestSnapshot(entityId);
var state = snapshot ? snapshot.state : {};
var fromOffset = snapshot ? snapshot.offset + 1 : 0;
var events = await log.readFrom(entityId, fromOffset);
return events.reduce(applyEvent, state);
}
// Take a snapshot periodically to keep startup fast
async function takeSnapshot(entityId, state, offset) {
await store.saveSnapshot({ entityId, state, offset, ts: Date.now() });
}
Replaying every event from offset zero to reconstruct state is correct but slow for long-running entities. Periodic snapshots cut the replay to only the events since the last checkpoint. The startup process loads the latest snapshot, fetches events written after its offset, and applies them. This keeps startup latency bounded regardless of how long the entity has existed. The same catch-up mechanism from realtime sync is the same pattern applied at the architecture level: snapshot plus delta.
Consumer offsets
A consumer offset is a pointer into the log. It decouples reading from writing and lets any number of consumers read the same log independently.
Independent consumers at their own pace
// Each consumer tracks its own position
var offset = parseInt(localStorage.getItem('stream-offset') || '0', 10);
function processEvent(event) {
applyEvent(localState, event);
offset = event.offset;
// Persist offset so a page reload continues from here
localStorage.setItem('stream-offset', offset);
}
// On reconnect: request events from saved offset
ws.addEventListener('open', function () {
ws.send(JSON.stringify({ type: 'subscribe', fromOffset: offset }));
});
Each consumer owns its offset. Two consumers reading the same log never interfere with each other. A slow consumer simply lags behind without blocking the fast one. For a frontend client, the offset acts as the catch-up cursor described in the sync guide: on reconnect, send the last offset and receive only the events missed during the disconnection. Storing the offset in localStorage across page reloads keeps the client from reprocessing the entire log on every visit.
Log compaction
An unbounded append-only log grows forever. Compaction trims it while preserving the information needed for new consumers to reconstruct current state.
Keeping the last event per key
Compaction scans the log and removes events that have been superseded, keeping only the most recent event for each entity key. A document renamed three times has three rename events in the uncompacted log; after compaction it has one, the final name. A new consumer reading the compacted log still reconstructs current state correctly, because the last event for each key is the only one that matters for the current value. Events that represent facts with no successor, a deletion, a creation, remain untouched. Log compaction is the mechanism that lets event-sourced systems serve new clients efficiently without a separate snapshot store.
The frontend stream client
From the browser's perspective, an event stream is a sequence of typed messages that arrive over a WebSocket or SSE connection and are reduced into local state.
A typed stream reducer
var state = {};
var offset = 0;
var handlers = {
'doc.created': function (s, p) { s[p.id] = { title: p.title }; },
'doc.renamed': function (s, p) { if (s[p.id]) s[p.id].title = p.title; },
'doc.archived': function (s, p) { if (s[p.id]) s[p.id].archived = true; }
};
ws.addEventListener('message', function (e) {
var event = JSON.parse(e.data);
if (event.offset <= offset) return; // deduplicate
var handler = handlers[event.type];
if (handler) handler(state, event.payload);
offset = event.offset;
render(state);
});
The reducer pattern keeps the client logic clean: each event type maps to a pure function that updates state, and the client never reads state from the server directly. It only ever receives events and derives state from them. This makes the client predictable, testable, and easy to debug because any state is reproducible by replaying the events that produced it. The render call after each event is where Backbone's event-driven views connect: a model.set or collection.reset after the reduction triggers all subscribed views automatically.
When to use event streaming
Event streaming adds complexity. The investment pays off in specific situations.
Audit, undo, and collaborative history
Use event streaming when the history of changes is itself valuable, not just the current state. Audit logs, undo stacks, time-travel debugging, and collaborative editing history are all natural fits. Use it when multiple independent consumers need to read the same changes without coordinating with each other, as the frontend client, a background indexer, and an analytics pipeline all consuming the same document event stream do. Use it when catch-up on reconnect must be robust: the log is the authoritative recovery mechanism and no special catch-up path is needed. Avoid it for ephemeral data, like presence and cursor positions, where history has no value and the overhead of a log is pure cost.
Frequently Asked Questions
What is an event streaming architecture?
Event streaming architecture stores all changes as an ordered, append-only log of events. Consumers read the log at their own pace, tracking their position with an offset. The current state of any entity can be reconstructed by replaying the events that describe its history.
What is event sourcing?
Event sourcing is the practice of storing the history of state changes as the primary record, not the current state. Every mutation is recorded as an immutable event. The current state is derived by replaying events from the beginning or from a snapshot checkpoint.
What is a consumer offset?
A consumer offset is the position in the event log that a consumer has read up to. It lets the consumer resume from where it left off after a restart or reconnection, without reprocessing events it has already handled or missing events that arrived while it was away.
What is log compaction?
Log compaction removes obsolete events from the log, keeping only the most recent event for each key. It lets the log grow indefinitely in terms of event count while keeping the storage size bounded. A new consumer can read the compacted log and reconstruct current state without replaying the entire history.
Read next: the Interactive Web hub, or continue to realtime data synchronization for the catch-up mechanics this architecture powers.
Want event-driven Backbone models that reduce over a live stream?
The Backbone guide shows how models and collections integrate with event-based data sources.
Explore the Backbone Guide →