Lucas Aragón 7 min readMost applications remember what is true now. An invoice is marked approved. A reservation is cancelled. An AI-generated recommendation is accepted. The database holds the latest answer, but often discards the sequence of decisions that produced it.
Event sourcing reverses that design. Instead of treating current state as the primary record, it stores each meaningful change as an immutable event. Current state becomes a view calculated from those events.
This distinction matters wherever software must explain itself, accommodate changing rules, or coordinate human and machine decisions. It also introduces real complexity. The useful question is not whether event sourcing is more advanced than conventional storage. It is whether history is part of the product.
The difference between storing state and storing change
Consider a procurement request. In a conventional database, one row might contain a status field with the value approved, an approver identifier, and a modification time. Each update replaces previous values unless separate audit machinery preserves them.
In an event-sourced system, the record is a sequence:
- RequestSubmitted
- RiskAssessmentRequested
- RiskAssessmentCompleted
- ApprovalGranted
Each event describes something that already happened. The application derives the request's current status by replaying the sequence in order. If ApprovalGranted follows the completed assessment, the resulting view is approved.
An event is not merely a log message. It is a durable domain fact with a defined schema, identity, timestamp, and position in a sequence. “Approval button clicked” describes interface activity. “ApprovalGranted” records the business consequence. That distinction keeps the history meaningful even after the interface changes.
The four working parts of an event-sourced system
| Part | Responsibility | Example |
|---|---|---|
| Command | Expresses an intention that may be accepted or rejected | ApproveRequest |
| Aggregate | Applies business rules to the relevant history | Procurement request |
| Event store | Appends accepted facts in sequence | ApprovalGranted at version 4 |
| Projection | Transforms events into a view optimized for reading | Approved requests dashboard |
A command is not an event. A user may issue ApproveRequest, but the system can reject it because an assessment is missing. Only accepted outcomes enter the event store.
The aggregate protects consistency. It loads the events for one business entity, reconstructs its state, evaluates the command, and emits new events. The event store appends those events only if no competing writer has changed the sequence in the meantime.
Projections make the system practical. Replaying every event for every screen would be inefficient, so consumers maintain disposable read models: tables for dashboards, search indexes, notification queues, or analytical views. These can be rebuilt from the source history when their logic changes.
A worked example: an AI-assisted approval
Suppose a company uses an AI model to flag procurement requests that may require additional review. The model advises; a human makes the final decision.
A request for a new software vendor begins with this event:
RequestSubmitted: request ID PR-104, vendor Acme Tools, category software, submitted by employee E-17.
The workflow asks a model to assess the request. When the result arrives, the system should not overwrite a risk field. It records the conditions surrounding the recommendation:
RiskAssessmentCompleted: assessment ID RA-88, request PR-104, model release M-12, policy version P-7, outcome additional-review, reason codes data-access and subcontractor-unknown.
A reviewer examines the evidence and accepts the vendor after confirming its subcontractor policy:
ApprovalGranted: request PR-104, reviewer E-42, assessment RA-88, policy version P-7.
From these events, one projection produces the operational view:
| Request | Vendor | AI recommendation | Final status | Reviewer |
|---|---|---|---|---|
| PR-104 | Acme Tools | Additional review | Approved | E-42 |
A second projection can answer a different question: how frequently do reviewers depart from recommendations made by model release M-12? No original decisions need to be reconstructed from overwritten fields. The relationship between recommendation and judgment is already explicit.
Now imagine policy P-8 changes the treatment of subcontractors. The company can replay historical requests through a new projection to identify cases that would receive different handling under the new policy. The original decisions remain untouched; the new interpretation lives beside them.
Design events as durable facts, not temporary payloads
Event names and contents become long-lived contracts. A useful event records enough context to preserve meaning without copying an entire database snapshot.
- Use past tense. Events report facts: PaymentAuthorized, ReviewRequested, RecommendationRejected.
- Capture decision context. Store relevant policy, model, rule, or workflow versions when they affect interpretation.
- Prefer domain meaning. StatusChanged is weaker than ApprovalRevoked because consumers must infer what the generic transition meant.
- Include stable identifiers. Entity, actor, correlation, causation, and event identifiers allow a decision chain to be traced.
- Avoid hidden dependence. If an event's meaning depends on mutable external data, preserve the necessary reference or decision-time value.
Do not store sensitive material merely because the log is comprehensive. Immutable histories complicate deletion and privacy obligations. A safer design may keep personal data in a separately governed store and place only a stable reference in the event. Deleting or restricting that referenced record can reduce exposure while preserving the non-personal sequence.
Concurrency, duplication, and failure are part of the design
Event sourcing does not eliminate distributed-systems problems; it makes them visible.
Assume two reviewers load PR-104 when its event stream is at version 3. Both try to append a decision as version 4. The event store accepts the first append and rejects the second because the expected version no longer matches. The application reloads the history and determines whether the second command remains valid.
Consumers can also receive an event more than once, especially when delivery is retried. A projection must therefore be idempotent: processing the same event again should not double a payment, duplicate a notification, or increment a counter twice. Recording the last processed event position or each processed event ID is a common mechanism.
Read models may lag behind the event store. Immediately after approval, a dashboard could briefly show the earlier status. This is eventual consistency. Products must account for it with confirmation states, direct reads for critical actions, or clear boundaries between write acknowledgment and projected visibility.
How events evolve without rewriting history
Software schemas change; immutable events remain. Treating evolution as an afterthought creates brittle replay processes.
Suppose an early RiskAssessmentCompleted event contains only an outcome. A later version adds reason codes. There are three practical approaches:
- Upcasting: transform old event shapes into the current representation when they are read.
- Versioned handlers: retain logic that understands each event version.
- New events: introduce a distinct event when the business meaning, rather than merely the schema, has changed.
Upcasting suits compatible additions. A new event type is cleaner when semantics diverge. Silently editing old records should be exceptional because it weakens the central promise: the recorded past is stable.
Snapshots can accelerate aggregates with long histories. A snapshot stores derived state at a known stream version; only subsequent events need replaying. It is a cache, not the source of truth, and must be discardable.
When the architecture earns its complexity
Event sourcing is valuable when several of these conditions hold:
- The sequence of actions has business or regulatory significance.
- Rules will change, and historical decisions may need reinterpretation.
- Several downstream views must react to the same facts.
- Users need explanations richer than a current status field.
- Human decisions, automated rules, and model outputs must remain distinguishable.
It is a poor default for simple reference data, ordinary content pages, or systems where only current state matters. Teams must operate an event store, projections, schema evolution, replay tooling, privacy controls, and eventual consistency. A conventional database plus a focused audit table may solve the actual requirement with less machinery.
A disciplined way to begin
Start with one bounded workflow whose history already causes operational friction. Do not convert an entire application.
- Write the decisions the workflow must later explain.
- Name the domain events that would make those explanations possible.
- Define commands separately, including reasons they may be rejected.
- Build one aggregate with optimistic concurrency.
- Create one operational projection and one historical or analytical projection.
- Test full replay from an empty read database.
- Test duplicate delivery, out-of-order handling where applicable, and event-version upgrades.
- Define deletion, retention, and access rules before real personal data enters the log.
The deeper opportunity is not perfect auditability. It is temporal leverage. When software preserves meaningful change, yesterday's operations can support tomorrow's questions without pretending that the current database state is the whole truth. For systems shaped by evolving policies and AI-assisted judgment, that memory can become part of the product itself.
This post was drafted with AI assistance and reviewed against our editorial policy before publication. Corrections are made at the source, on the page, with the date shown.
Rate this article
Discussion
Comments are moderated. Read our editorial policy.