Skip to Content
Course content

139: Practice Exercise: Building a Simple Event-Sourced Order System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think about how a bank account actually works. If you look at your mobile app, you see a "Current Balance." That's a convenience, but it's not the source of truth. The real truth is the ledger—the long, immutable list of every single deposit and withdrawal you've ever made. If the bank lost your balance but kept the ledger, they could just start at zero and "replay" every transaction to figure out exactly how much money you have right now. That is exactly what event sourcing is.

In a standard C# CRUD application, we usually just overwrite the database row. We change OrderStatus from Pending to Shipped. But once you do that, the information that it was ever pending is gone. In event sourcing, we don't update state; we append events.

Mapping the Ledger to C#

To move this from a bank analogy into an Order system, we have to shift our thinking. Instead of a table of "Orders," we need a stream of events. Here is how that maps out:

  • The Transaction becomes an Event (e.g., OrderPlaced, ItemAddedToOrder).
  • The Ledger becomes the Event Store (a list or database table that only allows appends).
  • The Current Balance becomes the Projected State (the Order object we build by replaying those events).

Wiring Up the Order Aggregate

I've found that the cleanest way to handle this in C# is to separate the event (the data) from the logic (how the state changes). You want your events to be simple POCOs (Plain Old CLR Objects) or records. The Order class then "consumes" these events to build its current state.

public record OrderCreated(Guid OrderId, DateTime CreatedAt);
public record ItemAdded(string ProductName, decimal Price);
public record OrderShipped(DateTime ShippedDate);

public class Order
{
    public Guid Id { get; private set; }
    public decimal TotalAmount { get; private set; }
    public bool IsShipped { get; private set; }
    public List<string> Items { get; private set; } = new();

    // This is where the magic happens: the "Apply" method
    public void Apply(object @event)
    {
        switch (@event)
        {
            case OrderCreated e:
                Id = e.OrderId;
                break;
            case ItemAdded e:
                Items.Add(e.ProductName);
                TotalAmount += e.Price;
                break;
            case OrderShipped e:
                IsShipped = true;
                break;
        }
    }
}

Replaying the History

Now, when you want to "load" an order from your database, you don't fetch a single row. You fetch every event associated with that OrderId and loop through them. I like this approach because it gives you a perfect audit log for free. If a customer asks why their total is $150, you don't have to guess—you can literally show them the sequence of ItemAdded events.

// Imagine this comes from a database
var eventStream = new List<object> 
{ 
    new OrderCreated(Guid.NewGuid(), DateTime.UtcNow),
    new ItemAdded("Mechanical Keyboard", 120.00m),
    new ItemAdded("USB-C Cable", 30.00m),
    new OrderShipped(DateTime.UtcNow)
};

var order = new Order();
foreach (var @event in eventStream)
{
    order.Apply(@event);
}

Console.WriteLine($"Order {order.Id} total: {order.TotalAmount}. Shipped: {order.IsShipped}");

It feels like more work upfront, sure. You're writing more classes and doing more loops. But as your system grows, you'll realize that being able to "time travel" to see exactly what an order looked like last Tuesday is an absolute lifesaver during debugging.




📋 Practical Task

Implementing Order Cancellation Logic in an Event-Sourced System

Your task is to extend the event-sourced order system to handle order cancellations. However, there is a business rule: An order cannot be cancelled if it has already been shipped.

Please complete the following:

  • Create a new record called OrderCancelled containing a Reason string.
  • Add a bool IsCancelled property to the Order class.
  • Update the Apply method in the Order class to handle the OrderCancelled event.
  • Create a method in the Order class called CanCancel() that returns true only if the order has not been shipped and is not already cancelled.
  • Write a small test script that attempts to cancel an order after a OrderShipped event has been applied, and use the CanCancel() method to prevent the OrderCancelled event from being added to the stream.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.