Application Module

← Main | 한국어 →


API


Overview

The Sigilaris application module provides the core abstractions for building modular blockchain applications with compile-time schema validation, type-safe table access, and automatic dependency injection. It implements the blueprint-based architecture described in ADR-0009.

Why do we need the application module? Blockchain applications require modular state management where different features can be independently developed, tested, and composed. This module provides the type-level machinery to ensure correctness at compile time while enabling flexible composition patterns.

Key Features:

Quick Start (30 seconds)

import cats.effect.IO
import org.sigilaris.core.application.module.blueprint.*
import org.sigilaris.core.application.module.provider.TablesProvider
import org.sigilaris.core.application.state.{Entry, StoreF, Tables}
import org.sigilaris.core.application.support.compiletime.Requires
import org.sigilaris.core.application.transactions.*
import org.sigilaris.core.assembly.EntrySyntax.*
import org.sigilaris.core.codec.byte.ByteCodec
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

// Define schema
type MySchema = Entry["accounts", Utf8, BigInt] *: EmptyTuple
val accountsEntry = entry"accounts"[Utf8, BigInt]
val schema = accountsEntry *: EmptyTuple

// Create reducer (placeholder)
class MyReducer[F[_]] extends StateReducer0[F, MySchema, EmptyTuple]:
  def apply[T <: Tx](signedTx: Signed[T])(using
    requiresReads: Requires[signedTx.value.Reads, MySchema],
    requiresWrites: Requires[signedTx.value.Writes, MySchema],
    ownsTables: Tables[F, MySchema],
    provider: TablesProvider[F, EmptyTuple],
  ): StoreF[F][(signedTx.value.Result, List[signedTx.value.Event])] = ???

// Create blueprint
// val blueprint = new ModuleBlueprint[IO, "myModule", (Entry["accounts", Utf8, BigInt],), EmptyTuple, EmptyTuple](
//   owns = schema,
//   reducer0 = new MyReducer[IO],
//   txs = TxRegistry.empty,
//   provider = TablesProvider.empty
// )

That's it! The application module automatically:

Architecture: Blueprint → Module

The application module follows a two-phase design inspired by ADR-0009:

Phase 1: Blueprint (Path-Independent)

Blueprints are deployment-agnostic specifications that define:

Key insight: Blueprints don't know where they'll be deployed. This enables:

Phase 2: Module (Path-Bound)

Mounting a blueprint to a specific Path creates a StateModule:

Example:

val blueprint: ModuleBlueprint[IO, "accounts", ...] = ...

// Deploy to different paths
val mainAccounts = blueprint.mount[("app", "v1", "accounts")]
val testAccounts = blueprint.mount[("test", "accounts")]  // Isolated for testing

Why Two Phases?

  1. Separation of Concerns: Logic (blueprint) vs deployment (module)
  2. Type Safety: Path becomes part of module type for prefix checking
  3. Flexibility: Same blueprint, multiple deployments
  4. Zero Cost: All path computation done at compile time

Documentation

Core Concepts

Main Types

Blueprint (Path-Independent)

ModuleBlueprint: Single-module specification with owned and needed tables

class ModuleBlueprint[F[_], MName <: String, Owns <: Tuple, Needs <: Tuple, Txs <: Tuple](
  owns: Owns,                           // Runtime tuple of Entry instances
  reducer0: StateReducer0[F, Owns, Needs],
  txs: TxRegistry[Txs],
  provider: TablesProvider[F, Needs]    // Injected external tables
)

ComposedBlueprint: Multiple modules combined with routing

class ComposedBlueprint[F[_], MName <: String, Owns <: Tuple, Needs <: Tuple, Txs <: Tuple](
  owns: Owns,
  reducer0: RoutedStateReducer0[F, Owns, Needs],  // Routes by moduleId
  txs: TxRegistry[Txs],
  provider: TablesProvider[F, Needs],
  routeHeads: List[String]                         // Module names for routing
)

StateModule (Path-Bound)

Runtime module with instantiated tables at a specific path:

class StateModule[F[_], Path <: Tuple, Owns <: Tuple, Needs <: Tuple, Txs <: Tuple, R](
  tables: Tables[F, Owns],               // Instantiated StateTable instances
  reducer: R,                             // StateReducer or RoutedStateReducer
  txs: TxRegistry[Txs],
  tablesProvider: TablesProvider[F, Needs]
)

State Types

StoreF: Effectful state monad with error handling and state threading

// StoreF stacks three effects:
type Eff[F[_]] = EitherT[F, SigilarisFailure, *]       // Error channel
type StoreF[F[_]] = StateT[Eff[F], StoreState, *]     // State threading

// Full expansion:
type StoreF[F[_]] = StateT[
  EitherT[F, SigilarisFailure, *],  // Can fail with SigilarisFailure
  StoreState,                        // Threads StoreState (trie + log)
  *                                   // Result type parameter
]

// Usage in reducers:
def apply[T <: Tx](...): StoreF[F][(T#Result, List[T#Event])]
// Returns: StateT that threads StoreState and can fail with SigilarisFailure

Why this stack?

StoreState: Combines MerkleTrie state with access logging (ADR-0009 Phase 8)

case class StoreState(
  trieState: MerkleTrieState,        // Actual key-value trie
  accessLog: AccessLog               // Tracks reads/writes for conflict detection
)

AccessLog: Records table-level operations for parallel execution analysis

case class AccessLog(
  reads: Map[ByteVector, Set[ByteVector]],   // tablePrefix → keys read
  writes: Map[ByteVector, Set[ByteVector]]   // tablePrefix → keys written
):
  def conflictsWith(other: AccessLog): Boolean  // Detects W∩W or R∩W conflicts
  def readCount: Int                            // Total unique keys read
  def writeCount: Int                           // Total unique keys written

Key property: Prefix-free table prefixes ensure no false positives in conflict detection.

Transaction Model

Tx: Base trait for all transactions

trait Tx:
  type Reads <: Tuple    // Required tables for reads
  type Writes <: Tuple   // Required tables for writes
  type Result            // Transaction result type
  type Event             // Event log type

ModuleRoutedTx: Transactions with module-relative routing

trait ModuleRoutedTx:
  def moduleId: ModuleId  // Always module-relative: MName *: SubPath

Dependency System

TablesProvider: Supplies external tables to dependent modules

trait TablesProvider[F[_], Schema <: Tuple]:
  def tables: Tables[F, Schema]
  def narrow[Subset <: Tuple](using TablesProjection[F, Subset, Schema]): TablesProvider[F, Subset]

Requires: Compile-time proof that transaction needs are satisfied

trait Requires[Needs <: Tuple, Schema <: Tuple]

Use Cases

Real-World Example: Accounts Module (ADR-0010)

The Accounts module from ADR-0010 demonstrates a complete blueprint implementation:

import cats.Monad
import cats.effect.IO
import org.sigilaris.core.application.module.blueprint.*
import org.sigilaris.core.application.module.provider.TablesProvider
import org.sigilaris.core.application.state.{Entry, StoreF, Tables}
import org.sigilaris.core.application.support.compiletime.Requires
import org.sigilaris.core.application.transactions.*
import org.sigilaris.core.assembly.EntrySyntax.*
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

// Accounts module schema (simplified from ADR-0010)
type AccountsOwns = (
  Entry["accountInfo", Utf8, BigInt],     // name → AccountInfo
  Entry["nameKey", (Utf8, BigInt), BigInt], // (name, keyId) → KeyInfo
)

// Accounts module has no dependencies
type AccountsNeeds = EmptyTuple

// Transaction types for accounts
trait CreateNamedAccount extends Tx:
  type Reads = EmptyTuple
  type Writes = AccountsOwns
  def nameValue: Utf8
  def initialKeyId: BigInt

class AccountsReducer[F[_]: Monad] extends StateReducer0[F, AccountsOwns, AccountsNeeds]:
  def apply[T <: Tx](signedTx: Signed[T])(using
    requiresReads: Requires[signedTx.value.Reads, AccountsOwns],
    requiresWrites: Requires[signedTx.value.Writes, AccountsOwns],
    tables: Tables[F, AccountsOwns],
    provider: TablesProvider[F, AccountsNeeds],
  ): StoreF[F][(signedTx.value.Result, List[signedTx.value.Event])] = ???

// AccountsBP can be deployed anywhere
// val accountsBP = new ModuleBlueprint[IO, "accounts", AccountsOwns, AccountsNeeds, ...]

Module Dependencies: Group Module (ADR-0011)

The Group module depends on Accounts for coordinator validation:

import cats.Monad
import cats.effect.IO
import scala.Tuple.++
import org.sigilaris.core.application.module.blueprint.*
import org.sigilaris.core.application.module.provider.TablesProvider
import org.sigilaris.core.application.state.{Entry, StoreF, Tables}
import org.sigilaris.core.application.support.compiletime.Requires
import org.sigilaris.core.application.transactions.*
import org.sigilaris.core.assembly.EntrySyntax.*
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

// Group owns its tables
type GroupOwns = (
  Entry["groupData", Utf8, BigInt],           // groupId → GroupData
  Entry["groupMember", (Utf8, Utf8), Unit],   // (groupId, accountName) → Unit
)

// Group needs Accounts tables for coordinator validation
type GroupNeeds = Entry["accountInfo", Utf8, BigInt] *: EmptyTuple  // From AccountsBP

// Group transactions operate over GroupOwns ++ GroupNeeds
trait CreateGroup extends Tx:
  type Reads = GroupNeeds    // Check coordinator exists
  type Writes = GroupOwns     // Create group
  def groupId: Utf8
  def coordinator: Utf8  // Must exist in accountInfo

class GroupReducer[F[_]: Monad] extends StateReducer0[F, GroupOwns, GroupNeeds]:
  def apply[T <: Tx](signedTx: Signed[T])(using
    requiresReads: Requires[signedTx.value.Reads, GroupOwns ++ GroupNeeds],
    requiresWrites: Requires[signedTx.value.Writes, GroupOwns ++ GroupNeeds],
    ownsTables: Tables[F, GroupOwns],
    provider: TablesProvider[F, GroupNeeds],  // Injected accountInfo table
  ): StoreF[F][(signedTx.value.Result, List[signedTx.value.Event])] = ???

// GroupBP explicitly declares dependency on Accounts
// val groupBP = new ModuleBlueprint[IO, "group", GroupOwns, GroupNeeds, ...]

Key insight: Group reducer can access both:

Deployment Patterns

import cats.effect.IO
import org.sigilaris.core.application.module.blueprint.ModuleBlueprint

// Assume we have the blueprints
def accountsBP(): ModuleBlueprint[IO, "accounts", EmptyTuple, EmptyTuple, EmptyTuple] = ???
def groupBP(): ModuleBlueprint[IO, "group", EmptyTuple, EmptyTuple, EmptyTuple] = ???

// Pattern 1: Shared Accounts (both modules use same instance)
// val accountsModule = accountsBP().mount[("app", "shared")]
// val groupModule = groupBP().mount[("app", "group")]  
//   // with accountsModule.tables as provider

// Pattern 2: Sandboxed Accounts (group has isolated accounts)
// val groupAccounts = accountsBP().mount[("app", "group", "accounts")]
// val groupModule = groupBP().mount[("app", "group")]
//   // with groupAccounts.tables as provider

Building a Simple Module

import cats.Monad
import cats.effect.IO
import org.sigilaris.core.application.module.blueprint.*
import org.sigilaris.core.application.module.provider.TablesProvider
import org.sigilaris.core.application.state.{Entry, StoreF, Tables}
import org.sigilaris.core.application.support.compiletime.Requires
import org.sigilaris.core.application.transactions.*
import org.sigilaris.core.assembly.EntrySyntax.*
import org.sigilaris.core.codec.byte.ByteCodec
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

// Define domain type (use BigInt directly for simplicity)
// case class Account(balance: BigInt)

// Define schema
type MySchema = Entry["accounts", Utf8, BigInt] *: EmptyTuple
val accountsEntry = entry"accounts"[Utf8, BigInt]
val schema = accountsEntry *: EmptyTuple

// Create reducer
class AccountsReducer[F[_]: Monad] extends StateReducer0[F, MySchema, EmptyTuple]:
  def apply[T <: Tx](signedTx: Signed[T])(using
    requiresReads: Requires[signedTx.value.Reads, MySchema],
    requiresWrites: Requires[signedTx.value.Writes, MySchema],
    ownsTables: Tables[F, MySchema],
    provider: TablesProvider[F, EmptyTuple],
  ): StoreF[F][(signedTx.value.Result, List[signedTx.value.Event])] = ???

// Create blueprint
// val blueprint = new ModuleBlueprint[IO, "accounts", MySchema, EmptyTuple, EmptyTuple](
//   owns = schema,
//   reducer0 = new AccountsReducer[IO],
//   txs = TxRegistry.empty,
//   provider = TablesProvider.empty
// )

Module Dependencies

import cats.Monad
import cats.effect.IO
import scala.Tuple.++
import org.sigilaris.core.application.module.blueprint.*
import org.sigilaris.core.application.module.provider.TablesProvider
import org.sigilaris.core.application.state.{Entry, StoreF, Tables}
import org.sigilaris.core.application.support.compiletime.Requires
import org.sigilaris.core.application.transactions.*
import org.sigilaris.core.assembly.EntrySyntax.*
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

// Module A owns accounts
type SchemaA = Entry["accounts", Utf8, BigInt] *: EmptyTuple

// Module B depends on A's accounts
type OwnsB = Entry["balances", Utf8, BigInt] *: EmptyTuple
type NeedsB = SchemaA  // Depends on accounts from module A

class ModuleBReducer[F[_]: Monad] extends StateReducer0[F, OwnsB, NeedsB]:
  def apply[T <: Tx](signedTx: Signed[T])(using
    requiresReads: Requires[signedTx.value.Reads, OwnsB ++ NeedsB],
    requiresWrites: Requires[signedTx.value.Writes, OwnsB ++ NeedsB],
    ownsTables: Tables[F, OwnsB],
    provider: TablesProvider[F, NeedsB],  // Injected accounts table
  ): StoreF[F][(signedTx.value.Result, List[signedTx.value.Event])] = 
    // Can access both ownsTables (balances) and provider.tables (accounts)
    ???

Composing Modules

import cats.effect.IO
import org.sigilaris.core.application.module.blueprint.{Blueprint, ModuleBlueprint}

// Two independent modules (placeholders)
def accountsBP(): ModuleBlueprint[IO, "accounts", EmptyTuple, EmptyTuple, EmptyTuple] = ???
def balancesBP(): ModuleBlueprint[IO, "balances", EmptyTuple, EmptyTuple, EmptyTuple] = ???

// Compose into single blueprint with routing
// val composed = Blueprint.composeBlueprint[IO, "app"](
//   accountsBP(),
//   balancesBP()
// )

// Transactions must use ModuleRoutedTx for routing

Type Conventions

Schema Types

Schemas are tuples of Entry types:

import org.sigilaris.core.application.state.Entry
import org.sigilaris.core.codec.byte.ByteCodec.given
import org.sigilaris.core.datatype.Utf8

type MySchema = (
  Entry["accounts", Utf8, BigInt],
  Entry["balances", Utf8, BigInt],
)

Owns vs Needs

Path Types

Paths are tuples of string literals representing deployment location:

Design Principles

Blueprint Phase: Modules don't know their deployment path. This enables reusability and testing independence.

Runtime Phase: Mounting binds a blueprint to a path, computing table prefixes and instantiating StateTable instances.

Type Safety: All schema requirements are validated at compile time. Missing tables, duplicate names, or non-prefix-free schemas are rejected by the compiler.

Dependency Injection: TablesProvider enables clean separation between module definition and dependency wiring.

Next Steps

Limitations

References


© 2025 Sigilaris. All rights reserved.