Event sourcing has always been a tough sell. The pitch is great — store every change as an immutable event, derive state by replaying them, get a perfect audit trail and time travel for free. The reality has been less rosy: schema versioning, eventual consistency, projections to maintain, and a team that needs to think differently about every write.

For most teams the answer has been “interesting, but our boring CRUD model is fine.” That was usually the right call.

I think the math is shifting.


A two-minute refresher

In a CRUD system, you store the current state. When an order ships, you update a row. The previous state is gone.

In event sourcing, you store the facts: OrderCreated, ItemAdded, OrderShipped. The state is whatever you get when you replay those events in order. Want a different view of the same data? Build a new projection — a read model fed by the same event stream — and replay history into it.

You gain a complete causal record. You pay for it with more moving parts.


What it looks like in C#

A minimal sketch, so the rest of the post has something concrete to point at:

// Events — facts, in the past tense, immutable
public abstract record OrderEvent;
public record OrderCreated(Guid OrderId, Guid CustomerId, DateTime CreatedAt) : OrderEvent;
public record ItemAdded(Guid OrderId, string Sku, int Quantity, decimal UnitPrice) : OrderEvent;
public record OrderShipped(Guid OrderId, DateTime ShippedAt, string Carrier) : OrderEvent;

// Aggregate — state derived by folding events
public class Order
{
    public Guid Id { get; private set; }
    public List<OrderLine> Lines { get; } = new();
    public bool IsShipped { get; private set; }

    public static Order Replay(IEnumerable<OrderEvent> history)
    {
        var order = new Order();
        foreach (var @event in history) order.Apply(@event);
        return order;
    }

    private void Apply(OrderEvent @event)
    {
        switch (@event)
        {
            case OrderCreated e: Id = e.OrderId; break;
            case ItemAdded e: Lines.Add(new OrderLine(e.Sku, e.Quantity, e.UnitPrice)); break;
            case OrderShipped: IsShipped = true; break;
        }
    }
}

public record OrderLine(string Sku, int Quantity, decimal UnitPrice);

That’s the shape. Every aggregate in the system needs the same scaffolding. Every new event needs an Apply clause. Every projection needs to consume the same stream and build its own read model. Multiply by a few dozen aggregates and you see why teams walk away.


What AI actually changes

The pain points of event sourcing turn out to be exactly the kind of work Claude Code handles well.

Modeling the domain

Deciding what counts as an event is the hardest part. Claude Code is a genuinely useful sparring partner here — you describe the domain, it proposes events, you push back. The output has a clear shape (past-tense verbs, one fact per event) that’s easy for both sides to reason about.

I usually start by pasting a few user stories into the chat and asking “what events would I need?”. The first list is rarely the final list, but it gets the conversation going faster than a whiteboard.

Boilerplate

Once you’ve committed to a pattern, the next aggregate is mostly typing. One solid example like the Order above in your codebase, and you can ask Claude Code to “add a Shipment aggregate following the same pattern, with events ShipmentDispatched, ShipmentDelivered, ShipmentReturned — and get a working scaffold back, including the Apply switch and a stub repository. Minutes, not hours.

Projections

A new read model used to mean a separate week of work — design the schema, write the projector, replay history, validate. Now I describe the projection in plain English, point Claude Code at the event types, and let it draft the projector:

public class CustomerOrderSummaryProjection
{
    public void Project(OrderEvent @event, CustomerOrderSummary summary)
    {
        switch (@event)
        {
            case OrderCreated e:
                summary.TotalOrders++;
                summary.LastOrderAt = e.CreatedAt;
                break;
            case ItemAdded e:
                summary.LifetimeValue += e.Quantity * e.UnitPrice;
                break;
            case OrderShipped e:
                summary.ShippedOrders++;
                break;
        }
    }
}

Run the replay, see if the numbers match a sample of known customers. If they don’t, the event stream tells you exactly where the projection went wrong.

Tests as specification

Event sourcing fits the given/when/then format perfectly: given these past events, when this command arrives, expect these new events. That’s nearly pseudo-code — and a great target for AI-generated tests.

[Fact]
public void Cannot_ship_an_order_that_is_already_shipped()
{
    // Given
    var orderId = Guid.NewGuid();
    var history = new OrderEvent[]
    {
        new OrderCreated(orderId, CustomerId: Guid.NewGuid(), DateTime.UtcNow),
        new ItemAdded(orderId, Sku: "ABC-123", Quantity: 2, UnitPrice: 19.99m),
        new OrderShipped(orderId, DateTime.UtcNow, Carrier: "PostNL"),
    };
    var order = Order.Replay(history);

    // When / Then
    var act = () => order.Ship(carrier: "DHL");
    act.Should().Throw<InvalidOperationException>()
       .WithMessage("*already shipped*");
}

I ask Claude Code for “the negative cases I’m probably forgetting” and get a list worth reviewing every single time.

Debugging

Production bug? Dump the event stream for the affected aggregate, hand it to Claude Code, ask what happened. The full causal chain is right there — no more “I think this field used to be different before that release on Tuesday.”


Where it actually got me

A concrete example. I’m working on a small invoicing side project — events for InvoiceIssued, PaymentReceived, InvoiceCancelled, RefundIssued. Adding a new projection (overdue invoices per customer, with aging buckets) used to be a half-day job. Last week I described it to Claude Code in three sentences, it drafted the projector and a replay test, I tweaked two field names, and it ran green against several thousand events of history.

That’s not magic. It’s just that the parts I always dreaded — the mechanical translation from event types to projection code — turn out to be the parts AI is best at.


When it still doesn’t fit

None of this rescues event sourcing for genuinely simple domains. If your app is a form over a table, stay with CRUD. Adding events plus AI just to feel modern is a great way to ship complexity nobody asked for.

There’s also a category of pain AI doesn’t make go away. Schema evolution still requires thought — when an event shape changes, you have to decide whether to upcast old events, version them, or rewrite the stream. Claude Code can write the upcaster, but it can’t make the design call for you. Same for eventual consistency: it’s a real constraint, not a boilerplate problem.

But if you’ve ever looked at event sourcing and walked away because the cost-benefit didn’t quite work — it’s worth a second look. The cost side moved.


Try it yourself

Pick the smallest aggregate in your current codebase. The one with three or four state transitions, nothing fancy. Open Claude Code in that project and try this:

claude

Then:

“Sketch what this would look like as an event-sourced aggregate. Events as C# records, an Apply method, and a given/when/then test for the main happy path.”

Read what comes back. Not to ship it — just to see the shape. If the result makes you think “that’s less work than I expected,” you’ve answered the question this post was really asking.