At a glance
Legacy sources: CRUD over relational tables · Entity Framework or ADO.NET data access · Stored procedures with business rules · Status columns and flag fields · Audit tables and triggers · .NET Framework 1.0 to 4.8 · .NET Core and .NET 5+
Modern targets: Domain events and a transactional outbox · Read models and projections · Event-sourced aggregates for chosen contexts · KurrentDB · Marten on PostgreSQL · A relational append-only event table · Azure Cosmos DB change feed
First step: Free 1-hour consultation, then a fixed-scope assessment
U.S. delivery: 100% U.S. citizen workforce, U.S.-soil delivery
What to know before you modernize
Where event sourcing pays
Microsoft's Azure Architecture Center describes event sourcing as a complex pattern with significant trade-offs and says that for most systems and most parts of a system, traditional data management is sufficient. The places it pays are specific. First, audit and history, where you must show what happened and why. Second, temporal questions such as what the order looked like last Tuesday, answered by replaying events. Third, complex workflows with compensation, where a later event reverses an earlier one and the reversal is itself part of the record. Fourth, integration by events, where other systems subscribe instead of polling tables. Fifth, replay for new read models, where a report nobody planned can be built from history that already exists. If a context has none of these needs, event sourcing adds cost without a return. If it has two or three, it is worth a closer look.
Event sourcing and CQRS book →
Where it does not pay
Do not event-source reference data, settings, lookup tables or catalogs. Microsoft lists static and reference data as a poor fit, along with simple CRUD that does not need auditability, replay or historical reconstruction, prototypes and short-lived systems, and screens that need strongly consistent, real-time views. Reporting-only needs do not justify it either: a read model or warehouse feed handles those. If the only requirement is a record of who changed what, an audit trail is far cheaper than a new persistence model, and we cover that case separately. Teams with no experience in event-driven design also face a real cost, since event sourcing changes how you test, debug and operate. It is also costly to migrate to or from, which is a reason to pick contexts carefully rather than avoid the pattern entirely. Candidly, most legacy systems should event-source one context at most, and many should event-source none.
Audit trail for .NET, when that is all you need →
Find the right bounded context first
The decision is made per bounded context, so first find the contexts. Event Storming is a workshop method that puts domain experts and developers in one room to lay out domain events on a timeline, find the commands and policies between them, and mark where the language changes. The result shows which parts of the business already behave like event streams, such as orders, payments, claims or shipments, and which are plain data maintenance. Those boundaries then decide where an aggregate can own its rules and where a table is fine. Skipping this step is the usual way event sourcing goes wrong: teams event-source a whole CRUD schema, produce events named after column updates, and keep the complexity with none of the benefit. Microsoft makes the same point about event design: capture business intent, not just resulting state.
The incremental path on a running system
You do not need a big bang. Step one is to introduce domain events in the existing code and a transactional outbox: the business change and its event are saved in the same database transaction, and a separate worker publishes the event, which prevents lost events when the message broker is down. Microsoft documents the pattern and names NServiceBus and MassTransit as libraries with outbox support. Step two is to add read models or projections built from those events, so screens and reports stop querying the write tables. Step three is to event-source one aggregate in one context, with its events as the system of record, and leave everything else on tables. Each step ships on its own, can be reversed, and teaches the team before the next one. Stop at any step where the remaining benefit no longer justifies the cost.
Rewrite or strangle: choosing the route →
Migrating existing rows into an initial event stream
A table row has current state but no history, so the first event for an existing record cannot be a true record of what happened. The usual approach is a baseline event per aggregate, with a name like OrderImported, carrying the state at cutover, followed by real events from then on. If old history lives in audit tables, history tables or logs, it can be converted into events only when it is complete and trustworthy enough to represent business intent; otherwise keep it as an archive and say so. Run the import as a repeatable, idempotent job, so a failed run can be repeated or discarded. Compare the state rebuilt from events with the source row for every migrated record before you switch writes. During cutover, one side must be the system of record for each aggregate at any moment, and the other side is a projection of it, never a second writer.
Versioning, snapshots, idempotent consumers and rebuilds
Events are permanent, so plan for change. Microsoft lists tolerant deserialization, version identifiers, upcasting and, as a last resort, in-place migration; upcasting converts old payloads to the current shape when they are read. Marten documents the same approach, favoring compensating events over rewriting history. Long streams are replayed from the start unless you keep snapshots, which are an optimization and can be regenerated from the events. Consumers receive events at least once, so handlers must be idempotent: track the last processed sequence number per consumer and skip duplicates. Projections are disposable by design, so you must be able to rebuild them; Marten, for example, rebuilds through its async daemon and supports running old and new projection versions side by side. Practice a full rebuild before production needs one, and measure how long it takes.
Personal data, erasure and GDPR
An append-only store conflicts with laws that require deleting personal data, and Microsoft says to design for that tension from the start. It describes two approaches. Keep personal data outside the event store and reference it by identifier, so deleting it does not touch the stream. Where personal data cannot be separated from the events, use crypto-shredding: encrypt personal data in events with a per-subject key, and delete the key to make the data unrecoverable while the event structure stays intact. That adds encryption work on every read and write and requires strong key management. Which approach fits, and whether a given dataset must be erasable at all, is a legal question for your counsel, not an architecture one. We design the technical option; we do not give legal advice. The simplest safe rule is to keep personal data out of events unless the event cannot work without it.
Operational costs to expect
Event sourcing moves effort rather than removing it. Projections are eventually consistent, so screens and APIs must tolerate a delay between a command and its read model. Microsoft notes that there is no standard way to query events beyond reading a stream, which is why you build and operate projections, and that testing needs more surface, including projection, idempotency and schema evolution tests. You run a store and a broker or subscription mechanism, monitor projection lag, and handle poison events. You keep upcasters for as long as old events exist, which is forever. Conflicts that span aggregates need explicit business handling, such as a back order. Staff need to learn given-when-then testing and event design. None of this is a reason to refuse the pattern in a context that needs it, but all of it belongs in the estimate, and in the decision about which contexts to include.
Choosing an event store
Microsoft describes two kinds of store: purpose-built event databases with built-in stream reads, optimistic concurrency and snapshots, and general-purpose relational or document databases with an append-only table, where you build those behaviors yourself. Kafka-style brokers are a distribution layer, not an event store. KurrentDB, the database formerly named EventStoreDB, is a purpose-built option; version 25.0 was the first release under the new name and changed the license from Event Store License v2 to Kurrent License v1, so read the current license. Kurrent Cloud runs on AWS, Azure or GCP, and official clients include .NET. Marten is a .NET library that turns PostgreSQL into an event store with inline, async and live projections. On Azure, a Cosmos DB change feed with Service Bus is a documented outbox implementation. Check that the client supports your runtime, since a .NET Framework app and a .NET 10 app may not have the same choices.
How the book and reference implementation relate
Thomas Jaeger wrote the book Event Sourcing and CQRS, which has 18 chapters, 72 diagrams and 79 code listings. The Kindle edition is delivered by Amazon from November 1, 2026. It is accompanied by a public, MIT-licensed reference implementation for .NET 10 and C# 14. The implementation models an order-management domain with five bounded contexts, puts four event stores behind one interface, and records its decisions in 53 architecture decision records. It is a reference codebase you can read and run, not a framework you must adopt, and the advice on this page does not depend on it. If you want to understand the pattern before deciding, the book is the longer treatment of what this page summarizes.
Event Sourcing and CQRS book →
Start with an assessment for an event sourcing candidate
The Legacy Software Assessment is a paid, fixed-scope, read-only review that follows a free one-hour consultation. For a .NET system considering event sourcing it inventories the data model and where business rules live, then identifies candidate bounded contexts and rates each against the criteria above: need for history, replay, compensation and integration. It finds where audit tables, triggers or polling already approximate events, lists the consumers that depend on current tables, and records personal data that would affect erasure design. It checks the runtime and hosting constraints that limit event store choices. The result states plainly which contexts should be event-sourced, which need only an outbox or an audit trail, and which should stay as tables. The fee is quoted after the consultation and is creditable toward a later modernization project.
Deliverables
- A map of candidate bounded contexts with a keep, outbox, or event-source verdict for each
- A list of current consumers and integrations that depend on the existing tables
- A migration approach for existing rows, including baseline events
- An event versioning, snapshot and projection rebuild plan
- A personal data and erasure design, for review by your counsel
- A comparison of event store options against your runtime and hosting constraints
Common questions
Can you add event sourcing to an existing .NET application?
Yes, but incrementally and only for the parts that need it. Start with domain events and a transactional outbox, add read models, then event-source one aggregate in one bounded context. Leave reference data and simple CRUD as tables. This works for .NET Framework systems as well as .NET Core and .NET 5 or later, subject to which event store client supports your runtime.
When should you not use event sourcing?
Skip it for reference data, lookup tables, simple CRUD without audit or replay needs, prototypes, short-lived systems, and screens that need strongly consistent real-time views. Microsoft's guidance says traditional data management is sufficient for most systems and most parts of a system. If you only need to know who changed what, an audit trail is cheaper.
What is the difference between event sourcing and an audit trail?
An audit trail records changes beside the current state, which remains the source of truth. In event sourcing the events are the source of truth, and current state is derived by replaying them. If you only need history for compliance, an audit trail is simpler and cheaper. Event sourcing earns its cost when you also need replay, compensation or new read models from history.
How do you migrate existing database rows to event sourcing?
Write a baseline event per aggregate carrying the state at cutover, then record real events from then on. Run the import as a repeatable job, compare state rebuilt from events against the source row, and keep exactly one writer per aggregate during cutover. Convert old audit history to events only if it is complete enough to represent real business intent.
How does event sourcing handle GDPR and the right to erasure?
Microsoft describes two approaches: keep personal data outside the events and reference it by identifier, or encrypt it with a per-subject key and delete the key, which is crypto-shredding. Both need design up front. Whether data must be erasable is a legal question, so confirm requirements with your counsel.
Which event store should we use with .NET?
It depends on your runtime, hosting and team. KurrentDB is a purpose-built event database with .NET clients. Marten gives you an event store on PostgreSQL. A relational append-only table works but you build stream reads and concurrency yourself. The assessment compares options against your constraints. Check each product's current license and client support before committing.
Does the book's reference implementation work for our system?
It is a teaching codebase, not a framework to adopt. It is MIT licensed, targets .NET 10 and C# 14, models an order-management domain with five bounded contexts, and puts four event stores behind one interface. It shows patterns you can adapt. Whether they fit your system is what the assessment decides.
How long does this take and what does it cost?
It depends on the number of contexts, the data volume and the integrations that depend on current tables. A free one-hour consultation comes first. Price and schedule are fixed after the fixed-scope assessment, and the assessment fee is creditable toward a modernization project. Project work ranges from a few weeks (virtualization) to several months (re-architecture).
Sources
- Event Sourcing pattern, Azure Architecture Center: When to use and avoid, versioning, upcasting, snapshots, idempotency, crypto-shredding, event store options
- Transactional Outbox with Azure Cosmos DB: Outbox pattern, change feed with Service Bus, NServiceBus and MassTransit outbox libraries
- Marten documentation: Event store and document database on PostgreSQL
- Marten events: Inline, async and live projections
- Marten event versioning: Upcasting and compensating changes
- Marten projection rebuilding: Rebuilds through the async daemon and side-by-side projection versions
- Kurrent documentation: KurrentDB server, Kurrent Cloud on AWS, Azure or GCP, official .NET client
- KurrentDB 25.0 release notes: First release under the KurrentDB name, license change to KLv1
Not sure where to start?
Start with a fixed-scope Legacy Software Assessment: a read-only review of your application’s source, database, configuration, logs, and architecture, ending in findings with evidence and a written recommendation. Quoted after the free 1-hour consultation; creditable toward a subsequent modernization project.