Skip to content

What's new

Insights on event sourcing, CQRS and related architectural patterns.

Dealing With Business Process Evolution - Versioning Business Logic in Event-Sourced Systems

Modern software does not just store data. It orchestrates business processes - approvals, fulfillments, onboarding flows - and these processes evolve. Regulations change, policies tighten, new approval steps appear, old thresholds shift. The code you wrote six months ago no longer represents the rules your business operates under today.

In the first article of this series, Evolving Event-Sourced Systems, I covered three strategies for dealing with schema evolution - calculating missing data through upcasting, compensating with sensible defaults, and enriching lazily. These approaches share a common assumption: the change is about data shape. New field appears, old events do not carry it, you bridge the gap at read time.

But what if the change is not about data, but about behavior? What if your loan approval process used to need one underwriter and now needs two? What if a regulatory body added a disclosure step that must happen at the moment an application is submitted? You cannot upcast your way out of this. The events from last month are still factually correct - they just happened under different rules.

This is where process version pinning enters the picture. Instead of trying to bend old events into new shapes, you record which version of the rules each process started under, and let every instance run to completion under its own pinned version. The event store stays immutable, the rules can evolve freely, and in-flight work finishes the way it began. That is the mechanism this article unpacks.

Evolving Event-Sourced Systems

Changing a data model is never trivial, regardless of the persistence strategy. In a traditional system, you write a migration script, add the new column, figure out how to backfill existing rows without breaking anything, and deploy. That process comes with its own challenges - choosing sensible defaults, coordinating with running applications, handling rollbacks - but at least the mechanism is familiar. Once the migration runs, every query sees the new structure.

Event sourcing introduces a different kind of challenge. Every state change in your system is stored as an immutable event, and those events accumulate over time - months, years, sometimes decades of recorded facts. This immutability is one of the core strengths of event sourcing: it gives you a complete audit trail, the ability to replay history, and the freedom to build new projections from existing data. But when a new requirement demands a field that did not exist six months ago, the immutability that protects your history also means you cannot simply alter what is already stored.

This is the schema evolution problem, and it is one of the first real challenges teams encounter after adopting event sourcing. There is a clear set of strategies for handling it, and none of them require modifying the events in the store. This article - the first in the System Evolution & Business Logic series - walks through three approaches: calculating missing data, compensating with defaults, and enriching lazily. By the end, you will know exactly which strategy to reach for when your schema needs to change.

The Write Model: Where Business Logic Lives

Every software system makes decisions. When a customer submits a loan application, something in your code decides whether that application is valid. When an employee approves a claim, something verifies that the approval is allowed. These decisions are the most important thing your software does - they are the reason the system exists.

When you think about software from the domain's perspective - what the business actually does, what decisions it makes, what rules it enforces - a natural question emerges: where in the code does all of this live? Domain-Driven Design encourages thinking in terms of business processes and decisions rather than data structures and technical layers. CQRS and event sourcing take this further by giving the decision-making part of your system an explicit, well-defined home: the write model.

The write model is the single, self-contained unit that takes the current state of your system, applies your business rules, and produces a decision - nothing gets written without passing through it first. In the previous article in this series, I explored read models - purpose-built data structures that transform your event stream into exactly the shape each consumer needs. This article looks at the other side of the equation: where read models answer "what should the user see?", the write model answers "is this operation allowed, and what happens as a result?"

One Truth, Many Views: Understanding Read Models in Event Sourcing

If you have spent any time building web applications, you are familiar with a pattern so ubiquitous that it feels like a law of nature. You have a database, you write data to it, and you read data from it - same tables, same schema, same source. Your SELECT queries run against the same rows your INSERT statements created. The mental model is simple: one database serves all purposes, and every part of your application sees the same data in the same shape.

Event sourcing breaks this assumption. In an event-sourced system, your write side does not store rows in a table - it stores a sequence of events in an event store. When a customer submits a loan application, you do not write an applications row with columns for name, amount, and status - instead, you store a LoanApplicationSubmittedEvent that captures the fact that something happened. Your write site replays these events to reconstruct the current state of the application, makes its decision, and publishes more events. The data is there, but it lives in a stream of facts, not in a queryable table.

This raises an immediate and practical question. Where does the data for your user interface come from? A bank employee needs a dashboard showing all pending applications with their risk categories. A customer needs a status page confirming that their application is under review.