Information systems as composable algebraic structures. Unifies state-stored (ποΈ store current state) and event-sourced (π store past events) architectures under a single model.
typealias StateStoredSystem<Command, State> = (Command, State?) -> State
typealias EventSourcedSystem<Command, InEvent, OutEvent> = (Command, List<InEvent>) -> List<OutEvent>The library is structured as a progression from the most general abstraction to practical implementations. Each file builds upon the previous one through specialization:
interface IGeneralSystem<in Command, in InState, out OutState, in InEvent, out OutEvent> {
val decide: (Command, InState) -> List<OutEvent>
val evolve: (InState, InEvent) -> OutState
val initialState: () -> OutState
// We CAN NOT convert general system into EventSourcedSystem !!!
// fun asEventSourcedSystem(): EventSourcedSystem<Command, InEvent, OutEvent> =
// { command, events ->
// decide(command, events.fold(initialState()) { acc, event -> evolve(acc, event) })
// }
// We CAN NOT convert general system into StateStoredSystem !!!
// fun asStateStoredSystem(): StateStoredSystem<Command, State> =
// { command, state ->
// val current = state ?: initialState()
// decide(command, current).fold(current) { acc, event -> evolve(acc, event) }
// }
}
interface IDynamicSystem<in Command, State, in InEvent, out OutEvent> :
IGeneralSystem<Command, State, State, InEvent, OutEvent> {
fun asEventSourcedSystem(): EventSourcedSystem<Command, InEvent, OutEvent> =
{ command, events ->
decide(command, events.fold(initialState()) { acc, event -> evolve(acc, event) })
}
// We CAN NOT convert dynamic system into StateStoredSystem !!!
// fun asStateStoredSystem(): StateStoredSystem<Command, State> =
// { command, state ->
// val current = state ?: initialState()
// decide(command, current).fold(current) { acc, event -> evolve(acc, event) }
// }
}
interface ISystem<in Command, State, Event> : IDynamicSystem<Command, State, Event, Event> {
fun asStateStoredSystem(): StateStoredSystem<Command, State> =
{ command, state ->
val current = state ?: initialState()
decide(command, current).fold(current) { acc, event -> evolve(acc, event) }
}
}Each level unlocks a new conversion, and the reason is purely type-level. The key operations are two folds:
Fold 1 β reconstruct state from events (needed by EventSourcedSystem):
events.fold(initialState()) { acc, event -> evolve(acc, event) }
initialState() returns OutState. evolve expects InState. If InState β OutState, feeding the accumulator back
in is a type error. This is why IGeneralSystem cannot produce an EventSourcedSystem β the loop can't close.
Fold 2 β fold output events back into state (needed by StateStoredSystem):
decide(command, current).fold(current) { acc, event -> evolve(acc, event) }
decide returns List<OutEvent>. evolve expects InEvent. If InEvent β OutEvent, you can't pass output events
into evolve. This is why IDynamicSystem (which already unifies state but not events) cannot produce a
StateStoredSystem.
Only ISystem, which unifies both (InState == OutState == State and InEvent == OutEvent == Event), can close both
folds and support both conversions.
| System | InState == OutState |
InEvent == OutEvent |
β EventSourcedSystem |
β StateStoredSystem |
|---|---|---|---|---|
IGeneralSystem |
β | β | β (Fold 1 breaks) | β |
IDynamicSystem |
β | β | β full (InEvent β OutEvent) | β (Fold 2 breaks) |
ISystem |
β | β | β limited (InEvent == OutEvent) | β |
The tradeoff at ISystem: constraining InEvent == OutEvent to unlock StateStoredSystem also weakens event
sourcing. IDynamicSystem supports heterogeneous event types β you can read a InEvent
and produce OutEvent. ISystem collapses these to a single Event type, so
the events you read must be exactly the events you write.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IGeneralSystem<Command, InState, OutState, InEvent, OutEvent> β
β (5 type parameters - most general) β
β β’ decide: (Command, InState) -> List<OutEvent> β
β β’ evolve: (InState, InEvent) -> OutState β
β β’ initialState: () -> OutState β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
β InState == OutState == State
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IDynamicSystem<Command, State, InEvent, OutEvent> β
β extends IGeneralSystem<Command, State, State, InEvent, OutEvent>β
β (4 type parameters) β
β β’ State types unified β
β β’ Can convert to EventSourcedSystem β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
β InEvent == OutEvent == Event
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ISystem<Command, State, Event> β
β extends IDynamicSystem<Command, State, Event, Event> β
β (3 type parameters - most common) β
β β’ Event types unified β
β β’ Can convert to StateStoredSystem β
β β’ Can convert to EventSourcedSystem (limited) β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
β Given-When-Then DSL
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SystemSpecification (Testing DSL) β
β β’ givenEvents / thenEvents (event-sourced, IDynamicSystem) β
β β’ givenState / thenState (state-stored, ISystem) β
β β’ whenCommand (identity for readability) β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
β Add persistence + metadata + transactions
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IStateRepository / IEventRepository β
β (Infrastructure layer - metadata + transaction support) β
β β’ Metadata never leaks into domain β
β β’ Explicit transaction handle (Txn), supplied via context param β
β β’ executeInTransaction (shared via ITransactional) / handle() β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
src/1_GeneralSystem.kt β The foundation
The most general form that captures all possible information systems:
interface IGeneralSystem<Command, InState, OutState, InEvent, OutEvent> {
val decide: (Command, InState) -> List<OutEvent>
val evolve: (InState, InEvent) -> OutState
val initialState: () -> OutState
}This abstraction allows:
- Different input/output state types (InState β OutState)
- Different input/output event types (InEvent β OutEvent)
- Full functorial and profunctor transformations
- Monoidal composition via
combine
src/2_DynamicSystem.kt β First specialization
Constrains state types to be equal: InState == OutState == State
interface IDynamicSystem<Command, State, InEvent, OutEvent> :
IGeneralSystem<Command, State, State, InEvent, OutEvent>This enables:
- Event-sourced systems (events can differ: InEvent β OutEvent)
- State reconstruction from event sequences
- Conversion to
EventSourcedSystemtype alias
src/3_System.kt β Second specialization
Further constrains event types to be equal: InEvent == OutEvent == Event
interface ISystem<Command, State, Event> :
IDynamicSystem<Command, State, Event, Event>This is the most common form, enabling:
- Both state-stored and event-sourced architectures
- Conversion to
StateStoredSystemtype alias - Conversion to
EventSourcedSystemtype alias (limited to InEvent == OutEvent) - Full bidirectional system representation
Note: The constraint InEvent == OutEvent means this system can only handle event-sourced scenarios where the events
you read are the same type as the events you produce. For more flexible event transformations (InEvent β OutEvent), use
DynamicSystem instead.
src/4_SystemSpecification.kt β Given-When-Then specification DSL
A lightweight DSL for specifying and verifying system behavior in both event-sourced and state-stored styles:
Event-sourced β given past events, when a command is issued, then expect new events:
counterSystem.givenEvents(emptyList()) {
whenCommand(IncrementCounterCommand.Increment(5))
} thenEvents listOf(IncrementCounterEvent.Incremented(5, 5))State-stored β given a current state, when a command is issued, then expect a new state:
counterSystem.givenState(CounterState(0, 0)) {
whenCommand(IncrementCounterCommand.Increment(5))
} thenState CounterState(incValue = 5, decValue = 0)| Function | Role | Works with |
|---|---|---|
givenEvents |
Given | IDynamicSystem (InEvent β OutEvent) |
givenState |
Given | ISystem (state-stored) |
whenCommand |
When | Any command type |
thenEvents |
Then | Asserts produced events match expected |
thenState |
Then | Asserts resulting state matches expected |
src/5_StatefulSystem.kt β Practical implementation
Adds repository interfaces for persistence with metadata and explicit transaction support:
interface ITransactional<Txn> {
suspend fun <T> executeInTransaction(block: suspend (Txn) -> T): T
}
interface IStateRepository<Command, CommandMetadata, State, StateMetadata, Txn> : ITransactional<Txn> {
context(txn: Txn)
suspend fun fetchState(command: Pair<Command, CommandMetadata>): Pair<State, StateMetadata>?
context(txn: Txn)
suspend fun save(state: Pair<State, CommandMetadata>): Pair<State, StateMetadata>
context(system: StateStoredSystem<Command, State>)
suspend fun handle(command: Pair<Command, CommandMetadata>): Pair<State, StateMetadata>
}
interface IEventRepository<Command, CommandMetadata, InEvent, InEventMetadata, OutEvent, OutEventMetadata, Txn> :
ITransactional<Txn> {
context(txn: Txn)
suspend fun fetchEvents(command: Pair<Command, CommandMetadata>): List<Pair<InEvent, InEventMetadata>>
context(txn: Txn)
suspend fun save(
events: List<OutEvent>,
commandMetadata: CommandMetadata
): List<Pair<OutEvent, OutEventMetadata>>
context(system: EventSourcedSystem<Command, InEvent, OutEvent>)
suspend fun handle(command: Pair<Command, CommandMetadata>): List<Pair<OutEvent, OutEventMetadata>>
}This provides:
- Separation of domain logic from persistence
- Explicit transaction handle (
Txn) for atomicity β the fetch β compute β save sequence runs within a single transaction, withTxnthreaded tofetchState/fetchEventsandsaveas a context parameter instead of an explicit argument - Flexible locking strategy: pessimistic (e.g.,
SELECT ... FOR UPDATE) or optimistic (e.g.,UPDATE ... WHERE version = ?) executeInTransactionfactored into a sharedITransactional<Txn>interface, since the transaction lifecycle is identical for both repositories β only the persisted shape (state vs. events) differs- Metadata support at infrastructure/application level
A key design principle: metadata exists only at the infrastructure/application boundaries and never leaks into the domain layer.
Domain systems (IGeneralSystem, IDynamicSystem, ISystem) remain pure and focused on business logic:
decide: (Command, State) -> List<Event> // No metadata hereMetadata is introduced at the repository level for infrastructure concerns:
- Versioning and optimistic locking
- Audit trails (who, when, from where)
- Correlation IDs for distributed tracing
- Event sequence numbers and timestamps
- Tenant/user context for multi-tenancy
The handle method orchestrates the flow:
- Fetch data with metadata from storage
- Extract pure domain data and pass to domain system (metadata-free)
- Domain system produces new data (pure business logic)
- Attach metadata and persist
This separation keeps domain logic testable, composable, and free from infrastructure concerns.
handle takes the domain system as a context parameter rather than an explicit argument, so a caller supplies it by
opening a context(...) block around the call:
context(orderRepository.asEventSourcedSystem()) {
orderRepository.handle(placeOrderCommand to userContext)
}This composition pattern:
- Explicit context, not hidden derivation: The domain system is derived once via
asEventSourcedSystem()/asStateStoredSystem()(available on anyIDynamicSystem/ISystem) and passed into scope explicitly at the call site - Separation of concerns: Repository provides persistence (
fetchEvents/fetchState,save,executeInTransaction), System provides domain logic - No inheritance required: Composition through interfaces, not class hierarchies
- Metadata isolation: The domain system extracted via
asEventSourcedSystem()/asStateStoredSystem()remains metadata-free
Example implementation:
class OrderSystem :
ISystem<OrderCommand, OrderState, OrderEvent>,
IEventRepository<OrderCommand, UserContext, OrderEvent, EventMetadata, OrderEvent, EventMetadata, Connection> {
// Domain logic (pure)
override val decide = { command: OrderCommand, state: OrderState -> emptyList<OrderEvent>() }
override val evolve = { state: OrderState, event: OrderEvent -> state }
override val initialState = { OrderState.empty() }
// Infrastructure (with metadata and transactions)
context(txn: Connection)
override suspend fun fetchEvents(command: Pair<OrderCommand, UserContext>) =
emptyList<Pair<OrderEvent, EventMetadata>>()
context(txn: Connection)
override suspend fun save(events: List<OrderEvent>, commandMetadata: UserContext) =
emptyList<Pair<OrderEvent, EventMetadata>>()
override suspend fun <T> executeInTransaction(block: suspend (Connection) -> T): T = block(connection)
}
// system is supplied explicitly via context - not derived automatically
context(orderSystem.asEventSourcedSystem()) {
val result = orderSystem.handle(placeOrderCommand to userContext)
}| Concept | Mathematical Structure | Notes |
|---|---|---|
mapCommand |
Contravariant Functor | Maps input command types |
mapEvent |
Profunctor (dimap) | Contravariant in input events, covariant in output |
mapState |
Profunctor (dimap) | Contravariant in input state, covariant in output |
combine |
Monoidal Product | Combines two systems into a product system |
emptySystem |
Monoidal Identity | Represents the "no-op" system (GeneralSystem<Nothing?, Unit, Nothing?>) |
As software engineering continues to raise the level of abstraction through AI, systems thinking becomes even more essential. While AI tools grow increasingly powerful at generating code, the real value lies in understanding how components interact, how decisions ripple through ecosystems, and how to design resilient, adaptable systems.
This library embodies that philosophy: composable algebraic structures that let you reason about system behavior at a higher level. When AI assists in implementation, your focus shifts to architecture, composition, and the invariants that matterβexactly where human insight creates the most value.
- Data (
examples/api.kt): Commands, Events, State - Behaviour (
examples/domain.kt): Exhaustive pattern matching on data

