I'm currently experimenting with rewriting the way domains are described in Sekiban, Event Sourcing and CQRS Framework, to be simpler and more flexible. I've been writing a variety of verification code for this. Over the past two and a half years, I've been adding and editing code, and it's started to feel a bit outdated. Moreover, there's a heavy use of reflection, making the code prone to bugs. Although I found it a hassle, I finally decided to sit down and implement the conceptual framework I had in mind from scratch.
This is the concept I had in mind:
When I first started writing Sekiban, I wasn’t as accustomed to organizing my interfaces all at once. This time, I was able to incorporate that approach right from the start, resulting in a relatively simple implementation.
After finishing the initial version, I discussed various ways we could refine the description method with my team. Now that it's taken shape, I've decided to put together a concept version in a repository.
Here it is:
https://github.com/J-Tech-Japan/SuperSimpleEventSourcing
Super Simple Event Sourcing
The data persistence layer is basically just an in-memory list of event objects. If we abstract this storage mechanism, supporting databases would be straightforward. For now, it's kept simple.
By defining aggregates, projectors, commands, events, and command handlers, you can easily run and experiment with event sourcing.
Since there's no thread control, there are no features to handle consistency in concurrent execution. However, if you manage threading, you could add controls to ensure that only one operation is executed per aggregate at a time.
Additionally, the repository includes samples for Web APIs, a console application, and unit tests. As shown in the GIF, we have a Web API implemented with a minimal amount of code.
The code itself is quite simple:
Framework: About 400 lines
Domain code: About 130 lines
Web API: About 30 lines
Console app: About 40 lines
Tests: About 100 lines
In total, it's roughly 700 lines of code.
The code is written in C#, but since it's not particularly complex, it might be easy to port it to another language. Starting from here, you could add persistence and thread control, potentially making it easier to introduce event sourcing into your projects.
Below is an English translation of the GitHub Readme.md. It also explains the sample code, so feel free to take a look if you're interested.
If you have any questions, feel free to reach out on X (formerly Twitter).
SuperSimpleEventSourcing
A Sample Event Sourcing Demo with Sekiban.Pure
This repository provides a very simple demonstration of event sourcing.
Event sourcing is a simple concept: save events, and project the current state using a projector.
Included here are a lightweight event sourcing library and a sample domain, making it a great starting point for understanding how event sourcing frameworks can be built.
It also comes with a minimal Web API (with a scalar UI), a simple console application, and straightforward unit tests.
Limitations
Events are stored in a simple List<IEvent>
—this is purely in-memory and not meant as a production-ready persistence layer. Additionally, we only have projection at the partition level, so you cannot create live projections that list all aggregates.
Project Overview
Domain: A simple event sourcing framework and sample domain.
Web: A basic Web API.
Console: A simple console application.
Test: Basic xUnit tests.
Note: Querying and listing existing aggregates is not currently implemented.
Key Components
Aggregate: Represents the state container, which changes throughout its lifecycle.
Command: Triggers changes to an Aggregate. Commands are processed by a command handler and affect only a single partition.
Command Handler: A function that produces events in response to a command.
Event: A fact that is stored as the source of truth.
Partition: A stream of events. Aggregates can be reconstructed (projected) from these events.
Projector: A function that applies events to evolve the aggregate’s state, returning a new state.
Below is a simple example using a "Branch" entity. It includes commands to register a branch and to change the branch's name.
public record Branch(string Name) : IAggregatePayload;
public record BranchCreated(string Name) : IEventPayload;
public record BranchNameChanged(string Name) : IEventPayload;
public class BranchProjector : IAggregateProjector
{
public IAggregatePayload Project(IAggregatePayload payload, IEvent ev) =>
(payload, ev.GetPayload()) switch
{
(EmptyAggregatePayload, BranchCreated created) => new Branch(created.Name),
(Branch branch, BranchNameChanged changed) => new Branch(changed.Name),
_ => payload
};
}
public record RegisterBranch(string Name) : ICommandWithHandler<RegisterBranch, BranchProjector>
{
public PartitionKeys SpecifyPartitionKeys(RegisterBranch command) => PartitionKeys<BranchProjector>.Generate();
public ResultBox<EventOrNone> Handle(RegisterBranch command, ICommandContext context) =>
EventOrNone.Event(new BranchCreated(command.Name));
}
public record ChangeBranchName(Guid BranchId, string NameToChange)
: ICommandWithHandler<ChangeBranchName, BranchProjector>
{
public ResultBox<EventOrNone> Handle(ChangeBranchName command, ICommandContext context) =>
context.AppendEvent(new BranchNameChanged(command.NameToChange));
public PartitionKeys SpecifyPartitionKeys(ChangeBranchName command) =>
PartitionKeys<BranchProjector>.Existing(BranchId);
}
Usage Examples
Register a Branch from the Console:
Console.WriteLine("Enter a new branch name:");
var inputN = Console.ReadLine();
var responseN = await executor.Execute(new RegisterBranch(inputN)).UnwrapBox();
var aggregateN = Repository.Load<BranchProjector>(responseN.PartitionKeys).UnwrapBox();
Console.WriteLine(JsonSerializer.Serialize(aggregateN.ToTypedPayload<Branch>().UnwrapBox()));
Change a Branch Name from the Console:
Console.WriteLine("ChangeName: Enter a new name:");
var input = Console.ReadLine();
if (!string.IsNullOrEmpty(input))
{
var response = await executor.Execute(new ChangeBranchName(responseN.PartitionKeys.AggregateId, input ?? "")).UnwrapBox();
var aggregate = Repository.Load<BranchProjector>(response.PartitionKeys).UnwrapBox();
Console.WriteLine(JsonSerializer.Serialize(aggregate.ToTypedPayload<Branch>().UnwrapBox()));
}
Minimal API Definition:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapGet("/", () => "Hello World!");
app.MapPost("/api/branch/register", async (RegisterBranch command) =>
{
var executor = new CommandExecutor { EventTypes = new DomainEventTypes() };
return await executor.Execute(command).UnwrapBox();
}).WithOpenApi();
app.MapPost("/api/branch/changename", async (ChangeBranchName command) =>
{
var executor = new CommandExecutor { EventTypes = new DomainEventTypes() };
return await executor.Execute(command).UnwrapBox();
}).WithOpenApi();
app.MapGet("/api/branch/{id}", (Guid id) =>
Repository.Load<BranchProjector>(PartitionKeys<BranchProjector>.Existing(id))
.Conveyor(aggregate => aggregate.ToTypedPayload<Branch>())
.UnwrapBox())
.WithOpenApi();
app.Run();
Next Steps
We are extending these concepts in the Sekiban project:
https://github.com/J-Tech-Japan/Sekiban
We are continually improving domain modeling approaches. While the concepts here are still under development, Sekiban already supports Azure Cosmos DB, DynamoDB, and PostgreSQL. It offers a full-featured environment for building event-sourced applications for small to medium projects, and we are working towards better support in distributed environments.