Information systems as composable algebraic structures. Unifies state-stored (ποΈ store current state) and event-sourced (π store past events) architectures under a single model.
The library is structured as a progression from the most general abstraction to practical implementations. Each file builds upon the previous one through specialization:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IGeneralSystem<Command, InState, OutState, InEvent, OutEvent> β
β (5 type parameters - most general) β
β β’ decide: (Command, InState) -> Sequence<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) β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
β Add persistence + metadata
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IStateRepository / IEventRepository β
β (Infrastructure layer - metadata support) β
β β’ Metadata never leaks into domain β
β β’ Transactional semantics (fetch β compute β save) β
β β’ handle() extension functions β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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) -> Sequence<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_StatefulSystem.kt β Practical implementation
Adds repository interfaces for persistence with metadata support:
interface IStateRepository<Command, CommandMetadata, State, StateMetadata> {
suspend fun fetchState(command: Pair<Command, CommandMetadata>): Pair<State, StateMetadata>?
suspend fun save(state: Pair<State, CommandMetadata>): Pair<State, StateMetadata>
suspend fun process(command: Pair<Command, CommandMetadata>, system: StateStoredSystem<Command, State>): Pair<State, StateMetadata>
}
interface IEventRepository<Command, CommandMetadata, InEvent, InEventMetadata, OutEvent, OutEventMetadata> {
suspend fun fetchEvents(command: Pair<Command, CommandMetadata>): Sequence<Pair<InEvent, InEventMetadata>>
suspend fun save(events: Sequence<OutEvent>, commandMetadata: CommandMetadata): Sequence<Pair<OutEvent, OutEventMetadata>>
suspend fun process(command: Pair<Command, CommandMetadata>, system: EventSourcedSystem<Command, InEvent, OutEvent>): Sequence<Pair<OutEvent, OutEventMetadata>>
}This provides:
- Separation of domain logic from persistence
- Transactional semantics (fetch β compute β save)
- Convenient
handleextension functions - 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) -> Sequence<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 process 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.
The handle extension functions demonstrate elegant composition at the type level:
suspend fun <SYSTEM, Command, State, Event, CommandMetadata, EventMetadata>
SYSTEM.handle(command: Pair<Command, CommandMetadata>): Sequence<Pair<Event, EventMetadata>>
where SYSTEM : IEventRepository<Command, CommandMetadata, Event, EventMetadata, Event, EventMetadata>,
SYSTEM : ISystem<Command, State, Event> =
process(command, inEventSourcedSystem())This composition pattern:
- Type-level composition: Any type implementing both
IEventRepositoryandISystemautomatically gainshandlecapability - Separation of concerns: Repository provides persistence, System provides domain logic
- No inheritance required: Composition through interfaces, not class hierarchies
- Metadata isolation: Domain system extracted via
inEventSourcedSystem()remains metadata-free
Example implementation:
class OrderSystem :
ISystem<OrderCommand, OrderState, OrderEvent>,
IEventRepository<OrderCommand, UserContext, OrderEvent, EventMetadata, OrderEvent, EventMetadata> {
// Domain logic (pure)
override val decide = { command: OrderCommand, state: OrderState -> /* ... */ }
override val evolve = { state: OrderState, event: OrderEvent -> /* ... */ }
override val initialState = { OrderState.empty() }
// Infrastructure (with metadata)
override suspend fun fetchEvents(command: Pair<OrderCommand, UserContext>) = /* ... */
override suspend fun save(events: Sequence<OrderEvent>, metadata: UserContext) = /* ... */
}
// Composition happens automatically - handle() is now available
val result = orderSystem.handle(PlaceOrder(...) to UserContext(userId = "123"))| 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

