DEV Community

Cover image for SpacetimeDB and Reducers
Khalid Hussein
Khalid Hussein

Posted on

SpacetimeDB and Reducers

SpacetimeDB is a transactional, in-memory relational database that lets you implement database-backed state machines via Rust reducers and table schemas. All data is stored in memory and persisted via an append-only Write-Ahead Log (WAL) called the Commitlog. Reducers are atomic, deterministic functions that execute inside the database, providing ACID guarantees.

In STK2ETH we use SpacetimeDB to store USSD menus, sessions, swaps, and audit logs.

Tables and Schema

Tables are declared using Rust structs annotated with the #[table(name = <table>)] macro, which generates typed accessors. This provides compile-time guarantees about columns and helps reduce human errors in row operations.

Example:

#[table(name = ussd_session, public)]
#[derive(Clone, Debug)]
pub struct UssdSession {
    #[primary_key]
    session_id: String,
    // ... other fields ...
}
Enter fullscreen mode Exit fullscreen mode

Important tables in STK2ETH:

  • ussd_menu, ussd_screen, menu_item: represent static menu structure loaded from a file.
  • ussd_session: session rows for active interactions.
  • swap: holds pending/executed swaps and transaction metadata.
  • eth_audit_logs: immutable audit trail for regulatory needs.

Reducer Semantics

Reducers are Rust functions annotated with #[reducer] that take a &ReducerContext as the first argument. They are applied deterministically and atomically, and can only return () or Result<(), E> where E: Display. This ensures state changes can be serialized consistently.

Example:

#[reducer]
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
    // reducer logic
}
Enter fullscreen mode Exit fullscreen mode

Guidelines:

  • Keep business logic testable: move pure logic (parsing, validation) into separate functions. Use reducers only for side-effects and DB writes.
  • Use stable string error codes when returning Err(String) from reducers to allow the UI to map errors to screens.
  • Avoid long-running synchronous operations in reducers. Use a table (e.g., ussd_request) to enqueue work for out-of-band workers when you need to call external services.

Deterministic Design and Side-Effect Management

Reducers must be deterministic and cannot perform direct network or filesystem I/O. External operations should be handled by enqueuing requests in a table, and a separate worker can process those requests and write results back to the database.

Pattern:

  1. Insert a ussd_request row describing the work.
  2. An off-chain worker reads the request, performs the external call, and inserts result rows for reducers to consume.

Testing Reducers

  • Unit test pure functions directly.
  • Reducer tests should be small and deterministic. Use the SpacetimeDB test harness for reducers that require database access (when available).

Example Patterns

  • Validation: Pure function validate_amount_value + reducer wrapper validate_amount that reads config, then returns Ok(()) or Err("amount_too_low").
  • Enqueueing: Reducers insert ussd_request rows and return immediately.

Observability

Reducers should log important events (e.g., session created, swap created, request enqueued) using SpacetimeDB’s logging system. This helps with debugging and auditing state transitions.

SpacetimeDB offers a deterministic and transactional pattern for implementing state machines. Treat reducers as the boundary for state changes, keep logic testable, enqueue external interactions for off-chain workers, and use stable error codes for UI mapping.

Top comments (0)