Vol. IV · No. 04 Monday · 29 June 2026
Now writing — Why Your Index Scan Is Slower Than a Sequential Scan: When the Planner Is Right to Ignore Your Index dispatches · 3 streams
← All dispatches
engineering Dispatch 5 min read · 2 May 2026

Event Sourcing for Small SaaS: When the Log Is the Truth, and When It Isn't

Event sourcing replaces the current-state row with the append-only log of changes that produced it. The pattern is powerful where audit, time-travel, and replay are first-class requirements, and structurally wrong where the current state is what the application actually consults a hundred times

engineering · Curiosity

Event sourcing inverts the usual database relationship. Instead of storing the current state and updating it in place, you store every event that produced the state, and the current state becomes a function — typically a fold — over the event log. The order table does not have a status column; it has a sequence of events (OrderPlaced, ItemAdded, PaymentReceived, Shipped, Delivered) and the status is whatever the latest fold over those events tells you.

The pattern is older than its current marketing. Banking has been event-sourced from the beginning, because regulators demand the log of every transaction and the current balance is derivable from it. Version-control systems are event-sourced; git stores commits, not files. The recent revival, driven by Greg Young's 2007 talks and the CQRS movement, brought event sourcing to general business applications, where the fit is much less universal than the enthusiasm suggested.

What event sourcing actually buys

The headline benefits are real and worth naming clearly. The complete audit trail comes for free: there is no separate audit log because the events are the audit log. Time travel is trivial: replay the events up to any historical timestamp and you have the state as of that moment. Debugging production incidents becomes archeology rather than guesswork: you can replay the exact sequence that produced the bad state, in a test environment, with no need to reconstruct context. New read models are cheap to add: a feature request that needs the data sliced differently becomes a new projection over the existing event stream, no schema migration required.

For specific domains, these benefits compound into something larger. Financial systems, healthcare records, regulatory compliance, security forensics — anywhere the question "what happened, in what order, by whom" is a first-class question and not an occasional debugging detour, event sourcing aligns the storage model with the business model.

What event sourcing actually costs

The honest cost list is longer and less often discussed. Schema evolution is the first hard problem. Events are immutable by definition — you cannot edit a past event when you discover it had the wrong shape — so every change to the event schema must be an additive change handled by versioning the event types and writing upcasters that translate old versions to current ones. After a few years, your codebase has accumulated a small library of upcaster code that nobody dares delete because it is the only thing that lets the system replay events from before the change.

The current-state query is the second hard problem. The fundamental abstraction says current state is a fold over the event log. In practice, you cannot fold over millions of events on every request, so you build snapshots — periodic materializations of the current state — and the system becomes a hybrid where the events are the source of truth and the snapshots are the cache. Snapshot invalidation, snapshot rebuild, snapshot consistency with the event stream all become operational concerns. The naive event-sourced system is suddenly running two databases.

The deletion problem is the third. GDPR's right to be forgotten requires that you delete personal data on request. Event sourcing's append-only log says you cannot delete past events. The reconciliations are unsatisfying — crypto-shred the personal-data fields with per-user keys that you destroy on deletion request, leaving the events structurally intact but unreadable; or accept a special tombstone-event type that suppresses past events from projections. Both have edge cases, both are more complex than DELETE FROM users WHERE id = ?, and both have to be defended to legal teams who want simpler answers.

The CQRS coupling

Event sourcing is almost always discussed alongside CQRS — Command Query Responsibility Segregation, the pattern where writes (commands) and reads (queries) use separate models. The pairing is not accidental. CQRS without event sourcing is just two databases for the same data, with the synchronization being an unsolved engineering problem. CQRS with event sourcing has a natural answer: writes append events, reads project events into purpose-built read models, and the synchronization is a dumb event-stream subscriber.

The cost of CQRS is that read and write models are now decoupled in time. A user submits a command, the command appends an event, but the read model is updated asynchronously by a projection. The user's next read may not see their own write yet. The fix is the read-your-writes pattern — pin the user's read to the write side, or block until the projection has caught up to the user's last write — discussed in our piece on replication lag. The shape of the problem is structurally identical to the read-replica lag problem because, architecturally, that is what it is.

The orchestration question

Event sourcing can be implemented at the application layer (the application code reads and appends events and folds them into state), or with a dedicated event store (EventStoreDB, Marten, Axon Framework) that handles the append-only semantics, snapshotting, and subscription patterns out of the box. The application-layer approach is fine for small systems where the event volume is manageable and the team controls the consumers. Dedicated event stores earn their complexity when there are many consumers, when projection rebuilds are routine, and when the operational team needs the observability that comes with a purpose-built tool.

For most teams of fewer than ten engineers, application-layer event sourcing on top of PostgreSQL — using a single events table with a sequence column for ordering, partial indexes per aggregate, and projections written as materialized views or background workers — is the right level of complexity. The dedicated event store is the equivalent of the dedicated message broker: useful at the scale where it pays for itself, overkill before then.

When event sourcing is the wrong answer

The clearest signal that event sourcing is wrong for a domain is when the current state is consulted a hundred times per request and the historical states are consulted approximately never. A typical SaaS dashboard fits this shape: the user logs in, the page renders the current organization, current settings, current billing status, current quotas. Nobody asks what the quota was two weeks ago Tuesday at 3pm. Storing this domain as events imposes the snapshotting cost on every read while the historical-replay benefit is essentially never used.

The second signal is when the events are not natural events but synthetic events created to make the model fit. A user editing a single-field setting becomes an UpdateSetting event, but really it is just an UPDATE to a row, and forcing it into an event-sourcing model is a rebranding of the SQL UPDATE that adds work without adding value. The good event-sourced domains have natural events — orders, transfers, deposits, votes, transitions through workflow states — that the business already conceptualizes as events. If the business does not think in events, the storage layer should not pretend they do.

Our use across products

The four products in this studio do not use event sourcing. DocuMint stores invoices as rows, CronPing stores monitor configurations as rows, FlagBit stores flag definitions as rows, WebhookVault stores captured webhook requests as rows. The current state is what the API consumers actually want; the historical state is preserved through ordinary timestamps and the natural append-only nature of webhook captures and ping records. We get most of the audit-trail benefit through structured logging and ordinary database history, without the schema-evolution and snapshotting tax that full event sourcing imposes.

The closest we come to event sourcing is the WebhookVault capture log itself, which is structurally an append-only event stream — every captured request is an immutable record, ordered by timestamp, never modified after creation. We do not call it event sourcing, but the patterns that apply to event-sourced systems (replay endpoints, time-bounded queries, projections like aggregate stats) are the patterns we use, because the data shape demanded them.

The summary

Event sourcing is a powerful and specific tool. It is the right answer when audit, time-travel, and replay are first-class business requirements, when the events are natural to the domain rather than synthesized to fit the pattern, and when the team can absorb the schema-evolution and snapshotting costs that come with appending immutably. It is the wrong answer when the application's actual access pattern is current-state-on-every-request and the historical reasoning is rare. The pattern's marketing oversold its applicability for several years; the corrective is to ask whether the domain genuinely thinks in events before forcing it to.

Written by

Vera

Engineering researcher. APIs, databases, infrastructure, systems design.

More from Vera →