One of the most formidable barriers in adopting and building event-sourced systems is learning to live with, and even embrace, the restrictions on component behavior. As it turns out, the same restrictions that make WebAssembly so powerful line up perfectly with event sourcing requirements.
Event sourcing, at its core, is a way of building applications that revolve around an immutable log of events. All state for such an application is derived by applying functions to that event stream. Event sourced applications not only know what the current state of the world is, but they know how it got there.
There have been entire books written about event sourcing, and I'm actually in the middle of writing one that will add to this list. Covering all there is to know about event sourcing is too much for a single blog post, so I hope this high-level overview encourages you to go explore further.
Building Blocks
Before we get into how event sourcing overlaps with Wasm components, let's go over the fundamental building blocks of any event-sourced application.
Aggregates
An event sourcing aggregate is a logical instance of an entity, derived from an event stream. While it shares the same name as Domain-Driven Design (DDD) aggregates, its meaning is a bit more focused. An aggregate could be an order, a bank-account, a ship or a player. Regardless of how aggregates are managed (typically in-memory or persisted and managed via snapshots), they perform two core functions.
- Process commands
- Derive state from events
An aggregate processes a command (typically an imperative like CreateOrder
or UpdateShip
etc) by validating it and then emitting one or more events in response to the command, like OrderCreated
or ShipUpdated
, etc.
From a functional programming standpoint, an aggregate is really two functions:
f(command, state) = [event]
f(event, state) = state'
I'll show an example of this when I get to the Wasm component code later in the post.
Projectors
Projectors are responsible for creating read model views by applying functions over an event stream. Projections (the output of a projector) are specifically designed to target O(1) query cost and support end users of the application.
Projectors don't maintain their own state, and must rely solely on event data, and so their functional analogy looks like this:
f(event) = [projection]
Process Managers
Process managers are stateful, though their state typically only lasts as long as some "long-running" process, where long running implies more than one event, and not the passage of clock time. You can think of them as an event-sourced specialization of the saga pattern, if you're familiar with that.
Process managers read in events, deriving internal state, and, where applicable, they issue commands to drive a process forward (or complete it).
f(event) = [command]
You might use a process manager to advance a new user provisioning process, perform multi-step transactions in a financial system or advance construction sites in a strategy game. The possibilities are endless!
Gateways (I/O)
An implicit requirement that I haven't talked about yet is that none of the building block components previously mentioned are allowed to perform side effects. Their operation needs to be functionally pure, otherwise all the powerful features of event sourcing will create disaster rather than predictability.
Whether you combine ingestion of events with publication of them into a single gateway or you use two different types of components, the desire is the same: only these types of components are allowed to ship events out of a closed system or submit commands into it.
Gateways are typically used to talk to other systems considered external, or to interact with systems that either don't follow event sourcing rules at all or embrace a more relaxed version (often called event-driven).
Combining Event Sourcing and Wasm
So what does any of this have to do with WebAssembly? It turns out that writing functionally pure code in Wasm is the default mode. If a component doesn't invoke imports that are satisfied by the host, then the component is literally unable to generate side-effects. This is where properties that look like limitations at first glance become incredibly powerful enabling traits when you get down into the details.
If we want to perform side effects, then we can simply grant our gateway components (input or output) the ability to make host calls. With frameworks like wasmCloud, this means we can securely grant those components access to a limited set of capabilities like a message broker or an HTTP server.
Now we can use our building blocks (aggregates, projectors, process managers, and gateways) to compose real applications without us having to worry about the typically gnarly details of plumbing event-sourced systems.
Using Concordance
Thankfully, we've created a wasmCloud capability provider called Concordance that does exactly that: makes it dead simple to compose resilient, highly distributable event-sourced apps using WebAssembly.
The examples in this post all come from our event-sourced bank account sample.
This blog post is using code from a version of Concordance that isn't available yet. It is waiting until a few key changes are committed to the wit syntax and to the component linker. The samples in the GitHub repository are modeled using Smithy, and will be migrated to wit as soon as is practical. We've used a code generator that will also be available as soon as we can reliably use the new wit/component libraries.
Let's take a look at a snippet of code for a bank account aggregate.
fn handle_create_account(cmd: CreateAccountCommand) -> Result<EventList>> {
vec![
AccountCreatedEvent {
initial_balance: cmd.initial_balance,
account_number: cmd.account_number.to_string(),
...
}
]
}
This couldn't be simpler (or more testable!). It's just a function that takes the create account command, validates it (not shown), and then returns a list of correlated events. Note that internal state of the aggregate isn't allowed to change during command handling. Let's see how that happens inside an event handler:
fn apply_account_created(event: AccountCreatedEvent) -> StateAck {
let state = AccountAggregateState {
balance: event.initial_balance,
min_balance: event.min_balance,
...
};
StateAck::ok(Some(state))
}
In the Rust version of the components, we can tell Concordance to set state by returning Some<T>
and purge it by returning None
. In keeping with wasmCloud's philosophy of abstracting away the non-functional requirements, component developers don't need to know how that state is being persisted: that's the job of the capability provider.
Lastly, let's take a look at a projector. Again, everything has been condensed down into its simplest form, letting developers focus on just the code that matters.
fn apply_account_created(event: AccountCreatedEvent) -> Result<ProjectorAck> {
let key = event.account_number;
// AccountLedger is just a plain old Rust struct
let ledger = AccountLedger::new(event.account_number, event.initial_balance);
let ledger_json = serde_json::to_string(&ledger).unwrap();
keyvalue::set(key, ledger_json)?;
Ok(ProjectorAck{error: None, succeeded: true})
}
Each one of these components can be built, linked, and deployed as a tiny .wasm
file. When we use wasmCloud for this, we also gain the additional security of cryptographic signatures via embedded JWTs, and the ability to deploy to a lattice: a unified, flat-topology, heterogeneous host runtime network.
What Next?
If you're eager to get started using Concordance, you can do so today using the code and examples from the GitHub repository. However, keep in mind that the Smithy-based syntax is going away and will soon be replaced with wit
-based syntax and compilation will target WebAssembly components.
Concordance is the event sourcing tool I've always wanted: define my business logic, declare my event model, deploy my components, and worry about nothing else. Hopefully you'll feel the same way and start experimenting with the event sourcing easy mode.