Skip to content

State Views

State Views are WebAssembly components that transform events into queryable state. They implement a fold/reduce function that processes events sequentially to build derived state, similar to materialized views in traditional databases but with custom logic.

Every State View implements a single function with this signature:

evolve(state, event) -> state

This function:

  • Takes the current state and an event
  • Returns the new state after processing the event
  • Is deterministic and side-effect free
  • Can be written in any language that compiles to WebAssembly

State Views process events in order:

  1. Start with an initial state (often empty/default)
  2. For each matching event, call the evolve function
  3. The output becomes the input for the next event
  4. Final state represents the materialized view

State Views support two temporal modes:

Revision Time (default):

  • State computed up to a specific database revision
  • Shows what was known at a point in time
  • Used for consistency and auditing

Effective Time:

  • State computed based on events’ effective timestamps
  • Shows business state at a specific moment
  • Used for bi-temporal queries

Here’s a complete example of an account summary State View:

use evidentsource_functions::prelude::*;
use serde::{Deserialize, Serialize};
// State type - serialized to/from JSON
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSummary {
pub id: String,
pub name: String,
pub balance: f64,
pub is_open: bool,
}
// Event data types for parsing
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AccountOpenedData {
account_id: String,
customer_name: String,
}
#[derive(Deserialize)]
struct AccountCreditedData {
amount: f64,
}
#[derive(Deserialize)]
struct AccountDebitedData {
amount: f64,
}
// Domain event enum
enum AccountEvent {
Opened(AccountOpenedData),
Credited(AccountCreditedData),
Debited(AccountDebitedData),
Closed,
Unknown,
}
// Parse CloudEvent into domain event
impl TryFrom<&Event> for AccountEvent {
type Error = String;
fn try_from(event: &Event) -> Result<Self, Self::Error> {
match event.event_type() {
"com.banking.account.opened" => {
let data: AccountOpenedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Opened(data))
}
"com.banking.account.credited" => {
let data: AccountCreditedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Credited(data))
}
"com.banking.account.debited" => {
let data: AccountDebitedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Debited(data))
}
"com.banking.account.closed" => Ok(AccountEvent::Closed),
_ => Ok(AccountEvent::Unknown),
}
}
}
// The state view function
#[evidentsource_functions::state_view(name = "account-summary")]
fn evolve(mut state: AccountSummary, event: AccountEvent) -> AccountSummary {
match event {
AccountEvent::Opened(data) => {
state.id = data.account_id;
state.name = data.customer_name;
state.is_open = true;
}
AccountEvent::Credited(data) => {
state.balance += data.amount;
}
AccountEvent::Debited(data) => {
state.balance -= data.amount;
}
AccountEvent::Closed => {
state.is_open = false;
}
AccountEvent::Unknown => {
// Ignore unknown events
}
}
state
}
package main
import (
"encoding/json"
"github.com/evidentsystems/evidentsource-sdks/go/packages/evidentsource-functions/adapters"
)
type AccountSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Balance float64 `json:"balance"`
IsOpen bool `json:"isOpen"`
}
type AccountSummaryView struct {
adapters.StateViewAdapter
}
func (sv *AccountSummaryView) Evolve(state []byte, event adapters.Event) ([]byte, error) {
var summary AccountSummary
if len(state) > 0 {
json.Unmarshal(state, &summary)
}
switch event.EventType() {
case "com.banking.account.opened":
var data struct {
AccountID string `json:"accountId"`
CustomerName string `json:"customerName"`
}
json.Unmarshal(event.Data(), &data)
summary.ID = data.AccountID
summary.Name = data.CustomerName
summary.IsOpen = true
case "com.banking.account.credited":
var data struct {
Amount float64 `json:"amount"`
}
json.Unmarshal(event.Data(), &data)
summary.Balance += data.Amount
case "com.banking.account.debited":
var data struct {
Amount float64 `json:"amount"`
}
json.Unmarshal(event.Data(), &data)
summary.Balance -= data.Amount
case "com.banking.account.closed":
summary.IsOpen = false
}
return json.Marshal(summary)
}
using EvidentSource.Functions.Adapters;
using EvidentSource.Core.Domain.Events;
public record AccountSummary(string Id, string Name, decimal Balance, bool IsOpen);
public abstract record AccountEvent
{
public sealed record Opened(string AccountId, string CustomerName) : AccountEvent;
public sealed record Credited(decimal Amount) : AccountEvent;
public sealed record Debited(decimal Amount) : AccountEvent;
public sealed record Closed : AccountEvent;
public sealed record Unknown : AccountEvent;
}
public sealed class AccountSummaryView : JsonStateViewAdapter<AccountSummary, AccountEvent>
{
protected override AccountSummary InitialState() =>
new AccountSummary("", "", 0m, false);
protected override AccountEvent? ParseEvent(Event evt) => evt.EventType switch
{
"com.banking.account.opened" => ParseEventData<OpenedData>(evt) is { } d
? new AccountEvent.Opened(d.AccountId, d.CustomerName) : null,
"com.banking.account.credited" => ParseEventData<CreditedData>(evt) is { } d
? new AccountEvent.Credited(d.Amount) : null,
"com.banking.account.debited" => ParseEventData<DebitedData>(evt) is { } d
? new AccountEvent.Debited(d.Amount) : null,
"com.banking.account.closed" => new AccountEvent.Closed(),
_ => new AccountEvent.Unknown()
};
protected override AccountSummary Evolve(AccountSummary state, AccountEvent evt) => evt switch
{
AccountEvent.Opened o => state with { Id = o.AccountId, Name = o.CustomerName, IsOpen = true },
AccountEvent.Credited c => state with { Balance = state.Balance + c.Amount },
AccountEvent.Debited d => state with { Balance = state.Balance - d.Amount },
AccountEvent.Closed => state with { IsOpen = false },
_ => state
};
private record OpenedData(string AccountId, string CustomerName);
private record CreditedData(decimal Amount);
private record DebitedData(decimal Amount);
}

State Views are configured via a metadata.json file:

{
"name": "account-summary",
"version": 1,
"description": "Account balance and status summary",
"eventSelector": {
"type": "or",
"selectors": [
{ "type": "eventTypeEquals", "value": "com.banking.account.opened" },
{ "type": "eventTypeEquals", "value": "com.banking.account.credited" },
{ "type": "eventTypeEquals", "value": "com.banking.account.debited" },
{ "type": "eventTypeEquals", "value": "com.banking.account.closed" }
]
},
"queryTemporality": "revisionTime"
}

Control which events are processed:

// Match specific event types
{
"type": "eventTypeEquals",
"value": "com.banking.account.opened"
}
// Match event types by prefix
{
"type": "eventTypeStartsWith",
"value": "com.banking.account."
}
// Match events by subject
{
"type": "subjectEquals",
"value": "acct-001"
}
// Combine selectors with OR
{
"type": "or",
"selectors": [
{ "type": "eventTypeEquals", "value": "OrderCreated" },
{ "type": "eventTypeEquals", "value": "OrderShipped" }
]
}
// Combine selectors with AND
{
"type": "and",
"selectors": [
{ "type": "subjectStartsWith", "value": "acct-" },
{ "type": "eventTypeEquals", "value": "com.banking.account.credited" }
]
}
Terminal window
rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
Terminal window
# Requires TinyGo 0.40.1+
tinygo build -target=wasip2 -o state-view.wasm .
Terminal window
dotnet workload install wasi-experimental
dotnet publish -c Release -p:BuildingForWasm=true

Upload the WASM component to the server:

Terminal window
curl -X POST http://localhost:3000/api/v1/databases/banking/state-views/account-summary/versions/1 \
--data-binary @target/wasm32-wasip2/release/account_summary.wasm \
-H "Content-Type: application/wasm"
Terminal window
# Get state view without parameters
curl http://localhost:3000/api/v1/databases/banking/state-views/order-summary/versions/1
# Get state view with parameters
curl "http://localhost:3000/api/v1/databases/banking/state-views/account-summary/versions/1?account_id=acct-001"
# Get state at a specific revision
curl "http://localhost:3000/api/v1/databases/banking/state-views/account-summary/versions/1?account_id=acct-001&revision=500"
import {
EvidentSource,
databaseName,
stateViewName,
stateViewContentAsJson,
stringAttribute,
} from "@evidentsource/client";
const es = EvidentSource.connect("http://localhost:50051");
const conn = await es.connectDatabase(databaseName("banking"));
const db = await conn.latestDatabase();
// Query with parameters
const params = new Map([["account_id", stringAttribute("acct-001")]]);
const view = await db.viewStateWithParams(
stateViewName("account-summary"),
1,
params
);
if (view) {
interface AccountSummary {
id: string;
name: string;
balance: number;
isOpen: boolean;
}
const account = stateViewContentAsJson<AccountSummary>(view);
console.log(`Balance: $${account.balance.toFixed(2)}`);
}
use evidentsource_client::EvidentSource;
use evidentsource_core::domain::{DatabaseName, StateViewName, EventAttribute};
let es = EvidentSource::connect_to_server("http://localhost:50051").await?;
let db_name = DatabaseName::new("banking")?;
let conn = es.connect(&db_name).await?;
// Query the state view
let sv_name = StateViewName::new("account-summary")?;
let account: Option<AccountSummary> = conn
.local_database()
.view_state_opt(&sv_name, 1)
.await?;
if let Some(acc) = account {
println!("Balance: ${:.2}", acc.balance);
}

Query state at different points in time:

// Get database at a specific revision
const historicalDb = await conn.databaseAtRevision(500n);
// Query state as it was known at that revision
const pastView = await historicalDb.viewStateWithParams(
stateViewName("account-summary"),
1,
params
);

For state views configured with effectiveTime query temporality:

Terminal window
# Query state as of a specific effective time
curl "http://localhost:3000/api/v1/databases/banking/state-views/account-summary/versions/1\
?account_id=acct-001&effective_time=2024-01-15T00:00:00Z"

Use test utilities to verify evolve logic:

#[cfg(test)]
mod tests {
use super::*;
use evidentsource_functions::testing::TestEventBuilder;
#[test]
fn test_account_opened() {
let state = AccountSummary::default();
let event = TestEventBuilder::new("com.banking.account.opened")
.subject("acct-001")
.json_data(&AccountOpenedData {
account_id: "acct-001".to_string(),
customer_name: "Alice".to_string(),
})
.build();
let domain_event = AccountEvent::try_from(&event).unwrap();
let new_state = evolve(state, domain_event);
assert_eq!(new_state.id, "acct-001");
assert_eq!(new_state.name, "Alice");
assert!(new_state.is_open);
}
#[test]
fn test_balance_calculation() {
let mut state = AccountSummary {
id: "acct-001".to_string(),
name: "Alice".to_string(),
balance: 100.0,
is_open: true,
};
// Credit
let credit = TestEventBuilder::new("com.banking.account.credited")
.json_data(&AccountCreditedData { amount: 50.0 })
.build();
state = evolve(state, AccountEvent::try_from(&credit).unwrap());
assert_eq!(state.balance, 150.0);
// Debit
let debit = TestEventBuilder::new("com.banking.account.debited")
.json_data(&AccountDebitedData { amount: 30.0 })
.build();
state = evolve(state, AccountEvent::try_from(&debit).unwrap());
assert_eq!(state.balance, 120.0);
}
}
  1. Keep state small: Large states impact query performance
  2. Use efficient serialization: JSON for simplicity, binary for performance
  3. Design for queries: Structure state to match query patterns
  4. Handle missing data: Use sensible defaults
  1. Be defensive: Handle malformed event data gracefully
  2. Ignore unknown events: Return state unchanged for unrecognized types
  3. Avoid side effects: Evolve must be pure and deterministic
  4. Log don’t fail: Log parsing errors but don’t crash
  1. Filter early: Use precise event selectors
  2. Minimize allocations: Reuse data structures where possible
  3. Consider partitioning: Use parameters to scope state views

State Views and State Changes form a feedback loop:

  1. State Change queries State View for current state
  2. State Change validates command against current state
  3. State Change emits events
  4. State View processes events to update state
  5. Next State Change sees updated state

This pattern enables:

  • Idempotency checking
  • Duplicate detection
  • Business rule validation
  • Aggregate enforcement