
Deterministic state-management architecture enforcing pure reducers, unidirectional data flow, explicit effects/events, formal algebraic guarantees enabling time‑travel debugging and boilerplate‑free side‑effect isolation.
Reduce. Conquer. Repeat.
Reduce & Conquer is a state management architecture for Kotlin Multiplatform and Compose Multiplatform. A
deterministic, boilerplate-free alternative to MVI and TEA patterns, it provides strict unidirectional data flow (UDF)
and robust side-effect isolation in commonMain — purpose-built for scalable multiplatform applications.
This repository contains the reference implementation and a comprehensive example demonstrating how the pattern integrates with Clean Architecture, using a Pokédex application built with Compose Multiplatform as the demonstration vehicle.
Add the dependency to your project:
// build.gradle.kts
commonMain.dependencies {
implementation("io.github.numq:reduce-and-conquer:1.0.0")
}Business logic resides in pure functions without side effects. Given the same State and Command, you always get the same
Transition — enabling time-travel debugging, reproducible testing, and formal verification.
All non-deterministic operations — network calls, database access, timers, reactive streams — are encapsulated in
declarative Effects. The reduction core remains pure, while effects are managed by the runtime engine.
Data flows strictly in a single loop: Command → Reducer → (State, Events, Effects) → Command. This eliminates circular dependencies and makes system transitions completely traceable.
Every architectural decision is backed by mathematical foundations of state machines and monoidal transition composition.
At the heart of the pattern lies a single, pure transition function:
reduce :: (State, Command) → (State, [Event], [Effect])
Action, Stream, Cancel), not executions.Event), and deferred work (Effect) do not
pollute one another.The pattern defines four core abstractions:
Action, Stream, Cancel) executed by the feature runtime.graph LR
Command["🗒️ Command"] --> Reducer["🧮 Reducer<br/>R: S×C→(S,Ev,Ef)"]
State["📊 State"] --> Reducer
Reducer --> NewState["📊 New State"]
Reducer --> Events["📨 Events"]
Reducer --> Effects["⚡ Effects"]
Effects -->|execute| Command
Events -->|notify| External["🌍 External Systems / UI"]While this repository provides a Kotlin Multiplatform library, Reduce & Conquer is fundamentally platform-agnostic:
interface Transition<S, C, E> {
state: S;
events: E[];
effects: Effect<C>[];
}
type Reducer<S, C, E> = (state: S, command: C) => Transition<S, C, E>;
const useFeature = <S, C, E>(
initialState: S,
reducer: Reducer<S, C, E>,
onEvent?: (event: E) => void
) => {
const processTransition = (transition: Transition<S, C, E>) => {
transition.events.forEach(e => onEvent?.(e));
transition.effects.forEach(effect => {
// execute side-effects and dispatch results
});
};
const [state, dispatch] = useReducer((currentState: S, command: C) => {
const transition = reducer(currentState, command);
setTimeout(() => processTransition(transition), 0);
return transition.state;
}, initialState);
const execute = useCallback((command: C) => {
dispatch(command);
}, []);
return {state, execute};
};struct Transition<State, Command, Event> {
let state: State
let events: [Event]
let effects: [Effect<Command>]
}
protocol Reducer {
associatedtype State
associatedtype Command
associatedtype Event
func reduce(state: State, command: Command) -> Transition<State, Command, Event>
}
@Observable
final class Feature<R: Reducer> {
private(set) var state: R.State
private let reducer: R
var events: AsyncStream<R.Event> { /* ... */ }
init(initialState: R.State, reducer: R) {
self.state = initialState
self.reducer = reducer
}
@MainActor
func execute(_ command: R.Command) {
let transition = reducer.reduce(state: state, command: command)
self.state = transition.state
transition.events.forEach { processEvent($0) }
transition.effects.forEach { effect in
Task {
if let nextCommand = await processEffect(effect) {
execute(nextCommand)
}
}
}
}
}class Transition<State, Command, Event> {
final State state;
final List<Event> events;
final List<Effect<Command>> effects;
Transition({
required this.state,
this.events = const [],
this.effects = const []
});
}
abstract class Reducer<State, Command, Event> {
Transition<State, Command, Event> reduce(State state, Command command);
}
class Feature<State, Command, Event> {
final Reducer<State, Command, Event> _reducer;
final _stateController = BehaviorSubject<State>();
Stream<State> get state => _stateController.stream;
Feature(State initialState, this._reducer) {
_stateController.add(initialState);
}
void execute(Command command) {
final currentState = _stateController.value;
final transition = _reducer.reduce(currentState, command);
_stateController.add(transition.state);
for (var effect in transition.effects) {
_processEffect(effect);
}
}
void _processEffect(Effect<Command> effect) async { /* ... */ }
}pub struct Transition<S, C, E> {
pub state: S,
pub events: Vec<E>,
pub effects: Vec<Effect<C>>,
}
pub trait Reducer: Send + Sync {
type State;
type Command;
type Event;
fn reduce(&self, state: Self::State, command: Self::Command)
-> Transition<Self::State, Self::Command, Self::Event>;
}
pub struct Feature<R: Reducer> {
state: Arc<Mutex<R::State>>,
reducer: Arc<R>,
command_tx: mpsc::UnboundedSender<R::Command>,
}
impl<R> Feature<R>
where
R: Reducer + 'static,
R::State: Send + Clone,
R::Command: Send,
R::Event: Send,
{
pub async fn execute(&self, command: R::Command) {
let mut state_guard = self.state.lock().unwrap();
let transition = self.reducer.reduce(state_guard.clone(), command);
*state_guard = transition.state;
drop(state_guard);
for effect in transition.effects {
self.process_effect(effect).await;
}
}
}type Transition[S any, C any, E any] struct {
State S
Events []E
Effects []Effect[C]
}
type Reducer[S any, C any, E any] interface {
Reduce(state S, command C) Transition[S, C, E]
}
type Feature[S any, C any, E any] struct {
mu sync.RWMutex
state S
reducer Reducer[S, C, E]
commands chan C
}
func (f *Feature[S, C, E]) Execute(command C) {
f.commands <- command
}
func (f *Feature[S, C, E]) loop() {
for cmd := range f.commands {
f.mu.Lock()
transition := f.reducer.Reduce(f.state, cmd)
f.state = transition.State
f.mu.Unlock()
for _, effect := range transition.Effects {
go f.processEffect(effect)
}
}
}import Control.Monad.State
import Control.Monad.Writer
import Data.DList (DList, fromList, toList)
data SideEffects c e = SideEffects
{ outEvents :: DList e
, outEffects :: DList (Effect c)
}
instance Semigroup (SideEffects c e) where
(SideEffects e1 f1) <> (SideEffects e2 f2) =
SideEffects (e1 <> e2) (f1 <> f2)
instance Monoid (SideEffects c e) where
mempty = SideEffects mempty mempty
type ReducerM s c e = c -> StateT s (Writer (SideEffects c e)) ()
emitEvent :: e -> StateT s (Writer (SideEffects c e)) ()
emitEvent e = tell $ SideEffects (fromList [e]) mempty
emitEffect :: Effect c -> StateT s (Writer (SideEffects c e)) ()
emitEffect f = tell $ SideEffects mempty (fromList [f])
runReducer :: ReducerM s c e -> s -> c -> (s, [e], [Effect c])
runReducer reducer s cmd =
let ((_, nextState), sideEffects) = runWriter (runStateT (reducer cmd) s)
in (nextState, toList $ outEvents sideEffects, toList $ outEffects sideEffects)classDiagram
class Feature~State, Command, Event~ {
<<interface>>
+StateFlow~State~ state
+Flow~Event~ events
+execute(command: Command): suspend*
+close()
}
class ReducerFeature~State, Command, Event~ {
-CoroutineScope scope
-Reducer~State, Command, Event~ reducer
-Channel~Command~ _commands
-MutableSharedFlow~Event~ _events
-Atomic~Map~ Any, Job~~ jobs
-processEffect(effect: Effect)
-launchManaged(key: Any, block)
-cancelJob(key: Any)
}
class Reducer~State, Command, Event~ {
<<interface>>
+reduce(state: State, command: Command): Transition~State, Event~*
+transition(state: State): Transition~State, Event~
}
class Transition~State, Event~ {
+State state
+List~Event~ events
+List~Effect~ effects
+event(event): Transition
+events(vararg events): Transition
+effect(effect): Transition
+effects(vararg effects): Transition
}
class Effect {
<<sealedinterface>>
}
class Effect_Stream~Command~ {
+Any key
+Flow~Command~ flow
+Strategy strategy
+((Throwable) -> Command) ? fallback
}
class Effect_Action~Command~ {
+Any key
+(() -> Command) block
+((Throwable) -> Command) ? fallback
}
class Effect_Cancel {
+Any key
}
Effect <|.. Effect_Stream
Effect <|.. Effect_Action
Effect <|.. Effect_Cancel
Feature <|.. ReducerFeature
ReducerFeature --> Reducer
ReducerFeature --> Transition
ReducerFeature ..> Effect
Reducer --> TransitionAn immutable data structure representing the current state of a component.
A message sent to the feature to request a state modification or trigger an effect.
A one-time notification for external subscribers (e.g. navigation actions, toasts, analytics).
A declarative description of an asynchronous side effect executed outside the reduction function.
Stream<out Command>: Subscribes to an external Flow<Command> with automated lifecycle management.Action<out Command>: Executes a one-off suspendable task with optional error fallback.Cancel: Immediately terminates an ongoing Action or Stream by its key.Sequential: Emits and processes commands sequentially (flow.collect).Restart: Cancels previous processing when a new emission occurs (flow.collectLatest).A lifecycle-aware container managing state emission, event propagation, and effect execution. It extendsAutoCloseable.
// Instantiation
val feature = Feature<SomeState, SomeCommand, SomeEvent>(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = SomeReducer(),
SomeCommand.LoadInitialData
)
// Delegation pattern (recommended)
class SomeFeature(reducer: SomeReducer) : Feature<SomeState, SomeCommand, SomeEvent> by Feature(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
SomeCommand.LoadInitialData
)A stateless functional interface:
interface Reducer<State, Command, Event in out> {
fun reduce(state: State, command: Command): Transition<State, Event>
fun transition(state: State) = Transition<State, Event>(state = state, events = emptyList(), effects = emptyList())
}transition(state): Initializes a Transition with empty event and effect lists.action(key, fallback) { ... }: Creates an Effect.Action.stream(key, flow, strategy, fallback): Creates an Effect.Stream.cancel(key): Creates an Effect.Cancel.class SomeReducer : Reducer<SomeState, SomeCommand, SomeEvent> {
override fun reduce(state: SomeState, command: SomeCommand) = when (command) {
is SomeCommand.HandleError ->
transition(state).event(SomeEvent.NotifyError(command.throwable.message.orEmpty()))
is SomeCommand.LoadData ->
transition(state).effect(
stream(
key = "data_stream",
flow = observeDataUseCase().map(SomeCommand::UpdateData),
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.UpdateData ->
transition(state.copy(data = command.data)).event(SomeEvent.ShowSuccess)
is SomeCommand.PerformAction ->
transition(state).effect(
action(
key = "save_action",
block = {
val result = repository.save()
SomeCommand.OnSaved(result)
},
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.Stop ->
transition(state).effect(cancel(key = "data_stream"))
}
}Let $S$ be the set of states, $C$ the set of commands, $Ev$ the set of events, and $Ef$ the set of effects.
The state transition function is defined as: $$R: S \times C \rightarrow S \times \mathcal{P} (Ev) \times \mathcal{P} (Ef)$$
Where: $$R (s, c) = (s', ev, ef)$$
For convenience, we denote the state projection as: $$s \cdot c = \pi_S (R (s, c))$$
Let $C^*$ be the free monoid of command sequences under concatenation ($\cdot$), with the empty sequence $\varepsilon$as identity: $$s \cdot \varepsilon = s$$ $$s \cdot (c_1 \cdot c_2) = (s \cdot c_1) \cdot c_2$$
Extending $R$ inductively over a sequence of commands $W = [c_1, c_2, \dots, c_n] \in C^$: $$R^(s, \varepsilon) = (s, \emptyset, \emptyset)$$ $$R^(s, [c_1]) = R (s, c_1) = (s_1, ev_1, ef_1)$$ $$R^(s, [c_1, c_2]) = (s_2, ev_1 \cup ev_2, ef_1 \cup ef_2) \quad \text{where } R (s_1, c_2) = (s_2, ev_2, ef_2)$$
By monoid action properties, grouping of consecutive sub-sequences is associative: $$R^(R_S^(s, W_1), W_2) = R^*(s, W_1 \cdot W_2)$$
This guarantees that folding commands in batches or sequentially produces the identical final state and equivalent accumulated collections of events and effects.
In general state management, commands are non-commutative (e.g. SetText("A") followed by Clear is not equivalent to
Clear followed by SetText("A")).
However, two commands $c_1, c_2 \in C$ are commutative if and only if their state transitions commute and their generated outputs are order-independent: $$s \cdot c_1 \cdot c_2 = s \cdot c_2 \cdot c_1$$ $$\pi_{Ev, Ef} (R^(s, [c_1, c_2])) = \pi_{Ev, Ef} (R^(s, [c_2, c_1]))$$
When this condition is satisfied: $$R^(s, [c_1, c_2]) = R^(s, [c_2, c_1])$$
The state transition system forms a deterministic monoid action on state $S$, guaranteeing:
Feature and Reducer eliminate monolithic controllers and mutable model state.Reducer is a pure function, keeping business logic strictly deterministic.Feature
transitions can only be triggered via formal Command dispatch.Event) from asynchronous work (Effect).Effect.Stream alongside single-shot
Effect.Action.Feature isolates internal state and processes commands sequentially via a Kotlin
Channel.Reduce & Conquer
concentrates all logic in a single pure reduction contract.graph LR
subgraph "Presentation Layer"
View["View (Compose)"] --> Feature["Feature"]
Feature["Feature"] --> Reducer["Reducer"]
end
subgraph "Domain Layer"
Reducer --> UseCase["Use Case"]
UseCase --> Entity["Entity"]
end
subgraph "Infrastructure Layer"
UseCase --> Repository["Repository"]
Repository --> External["Network / DB / Bluetooth"]
enddata class User(val id: String, val name: String)
data class UserState(val users: List<User> = emptyList())
sealed interface UserCommand {
data object LoadUsers : UserCommand
data class UpdateUsers(val users: List<User>) : UserCommand
data class HandleError(val throwable: Throwable) : UserCommand
}
sealed interface UserEvent {
data class NotifyError(val message: String) : UserEvent
data class ShowSuccess(val message: String) : UserEvent
}
class UserReducer(
private val observeUsersUseCase: ObserveUsersUseCase
) : Reducer<UserState, UserCommand, UserEvent> {
override fun reduce(state: UserState, command: UserCommand) = when (command) {
UserCommand.LoadUsers -> transition(state).effect(
stream(
key = "users_stream",
flow = observeUsersUseCase().map(UserCommand::UpdateUsers),
fallback = { throwable -> UserCommand.HandleError(throwable) }
)
)
is UserCommand.UpdateUsers ->
transition(state.copy(users = command.users)).event(UserEvent.ShowSuccess("Users updated"))
is UserCommand.HandleError ->
transition(state).event(UserEvent.NotifyError("Failed: ${command.throwable.message}"))
}
}
class UserFeature(reducer: UserReducer) : Feature<UserState, UserCommand, UserEvent> by Feature(
initialState = UserState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
UserCommand.LoadUsers
)
@Composable
fun UserScreen(feature: UserFeature) {
val state by feature.state.collectAsState()
LaunchedEffect(Unit) {
feature.events.collect { event ->
when (event) {
is UserEvent.NotifyError -> showSnackbar(event.message)
is UserEvent.ShowSuccess -> showSnackbar(event.message)
}
}
}
}Because business logic is encapsulated in pure functions, reducers can be verified synchronously without mocking coroutine scopes:
@Test
fun `load data produces stream effect`() {
val reducer = UserReducer(observeUsersUseCase)
val initialState = UserState()
val transition = reducer.reduce(initialState, UserCommand.LoadUsers)
assertEquals(initialState, transition.state)
assertEquals(0, transition.events.size)
assertEquals(1, transition.effects.size)
assertIs<Effect.Stream<*>>(transition.effects.first())
}A cross-platform Pokédex application demonstrating Reduce & Conquer in practice.
This project is licensed under the MIT License - see the LICENSE file for details.
Reduce. Conquer. Repeat.
Reduce & Conquer is a state management architecture for Kotlin Multiplatform and Compose Multiplatform. A
deterministic, boilerplate-free alternative to MVI and TEA patterns, it provides strict unidirectional data flow (UDF)
and robust side-effect isolation in commonMain — purpose-built for scalable multiplatform applications.
This repository contains the reference implementation and a comprehensive example demonstrating how the pattern integrates with Clean Architecture, using a Pokédex application built with Compose Multiplatform as the demonstration vehicle.
Add the dependency to your project:
// build.gradle.kts
commonMain.dependencies {
implementation("io.github.numq:reduce-and-conquer:1.0.0")
}Business logic resides in pure functions without side effects. Given the same State and Command, you always get the same
Transition — enabling time-travel debugging, reproducible testing, and formal verification.
All non-deterministic operations — network calls, database access, timers, reactive streams — are encapsulated in
declarative Effects. The reduction core remains pure, while effects are managed by the runtime engine.
Data flows strictly in a single loop: Command → Reducer → (State, Events, Effects) → Command. This eliminates circular dependencies and makes system transitions completely traceable.
Every architectural decision is backed by mathematical foundations of state machines and monoidal transition composition.
At the heart of the pattern lies a single, pure transition function:
reduce :: (State, Command) → (State, [Event], [Effect])
Action, Stream, Cancel), not executions.Event), and deferred work (Effect) do not
pollute one another.The pattern defines four core abstractions:
Action, Stream, Cancel) executed by the feature runtime.graph LR
Command["🗒️ Command"] --> Reducer["🧮 Reducer<br/>R: S×C→(S,Ev,Ef)"]
State["📊 State"] --> Reducer
Reducer --> NewState["📊 New State"]
Reducer --> Events["📨 Events"]
Reducer --> Effects["⚡ Effects"]
Effects -->|execute| Command
Events -->|notify| External["🌍 External Systems / UI"]While this repository provides a Kotlin Multiplatform library, Reduce & Conquer is fundamentally platform-agnostic:
interface Transition<S, C, E> {
state: S;
events: E[];
effects: Effect<C>[];
}
type Reducer<S, C, E> = (state: S, command: C) => Transition<S, C, E>;
const useFeature = <S, C, E>(
initialState: S,
reducer: Reducer<S, C, E>,
onEvent?: (event: E) => void
) => {
const processTransition = (transition: Transition<S, C, E>) => {
transition.events.forEach(e => onEvent?.(e));
transition.effects.forEach(effect => {
// execute side-effects and dispatch results
});
};
const [state, dispatch] = useReducer((currentState: S, command: C) => {
const transition = reducer(currentState, command);
setTimeout(() => processTransition(transition), 0);
return transition.state;
}, initialState);
const execute = useCallback((command: C) => {
dispatch(command);
}, []);
return {state, execute};
};struct Transition<State, Command, Event> {
let state: State
let events: [Event]
let effects: [Effect<Command>]
}
protocol Reducer {
associatedtype State
associatedtype Command
associatedtype Event
func reduce(state: State, command: Command) -> Transition<State, Command, Event>
}
@Observable
final class Feature<R: Reducer> {
private(set) var state: R.State
private let reducer: R
var events: AsyncStream<R.Event> { /* ... */ }
init(initialState: R.State, reducer: R) {
self.state = initialState
self.reducer = reducer
}
@MainActor
func execute(_ command: R.Command) {
let transition = reducer.reduce(state: state, command: command)
self.state = transition.state
transition.events.forEach { processEvent($0) }
transition.effects.forEach { effect in
Task {
if let nextCommand = await processEffect(effect) {
execute(nextCommand)
}
}
}
}
}class Transition<State, Command, Event> {
final State state;
final List<Event> events;
final List<Effect<Command>> effects;
Transition({
required this.state,
this.events = const [],
this.effects = const []
});
}
abstract class Reducer<State, Command, Event> {
Transition<State, Command, Event> reduce(State state, Command command);
}
class Feature<State, Command, Event> {
final Reducer<State, Command, Event> _reducer;
final _stateController = BehaviorSubject<State>();
Stream<State> get state => _stateController.stream;
Feature(State initialState, this._reducer) {
_stateController.add(initialState);
}
void execute(Command command) {
final currentState = _stateController.value;
final transition = _reducer.reduce(currentState, command);
_stateController.add(transition.state);
for (var effect in transition.effects) {
_processEffect(effect);
}
}
void _processEffect(Effect<Command> effect) async { /* ... */ }
}pub struct Transition<S, C, E> {
pub state: S,
pub events: Vec<E>,
pub effects: Vec<Effect<C>>,
}
pub trait Reducer: Send + Sync {
type State;
type Command;
type Event;
fn reduce(&self, state: Self::State, command: Self::Command)
-> Transition<Self::State, Self::Command, Self::Event>;
}
pub struct Feature<R: Reducer> {
state: Arc<Mutex<R::State>>,
reducer: Arc<R>,
command_tx: mpsc::UnboundedSender<R::Command>,
}
impl<R> Feature<R>
where
R: Reducer + 'static,
R::State: Send + Clone,
R::Command: Send,
R::Event: Send,
{
pub async fn execute(&self, command: R::Command) {
let mut state_guard = self.state.lock().unwrap();
let transition = self.reducer.reduce(state_guard.clone(), command);
*state_guard = transition.state;
drop(state_guard);
for effect in transition.effects {
self.process_effect(effect).await;
}
}
}type Transition[S any, C any, E any] struct {
State S
Events []E
Effects []Effect[C]
}
type Reducer[S any, C any, E any] interface {
Reduce(state S, command C) Transition[S, C, E]
}
type Feature[S any, C any, E any] struct {
mu sync.RWMutex
state S
reducer Reducer[S, C, E]
commands chan C
}
func (f *Feature[S, C, E]) Execute(command C) {
f.commands <- command
}
func (f *Feature[S, C, E]) loop() {
for cmd := range f.commands {
f.mu.Lock()
transition := f.reducer.Reduce(f.state, cmd)
f.state = transition.State
f.mu.Unlock()
for _, effect := range transition.Effects {
go f.processEffect(effect)
}
}
}import Control.Monad.State
import Control.Monad.Writer
import Data.DList (DList, fromList, toList)
data SideEffects c e = SideEffects
{ outEvents :: DList e
, outEffects :: DList (Effect c)
}
instance Semigroup (SideEffects c e) where
(SideEffects e1 f1) <> (SideEffects e2 f2) =
SideEffects (e1 <> e2) (f1 <> f2)
instance Monoid (SideEffects c e) where
mempty = SideEffects mempty mempty
type ReducerM s c e = c -> StateT s (Writer (SideEffects c e)) ()
emitEvent :: e -> StateT s (Writer (SideEffects c e)) ()
emitEvent e = tell $ SideEffects (fromList [e]) mempty
emitEffect :: Effect c -> StateT s (Writer (SideEffects c e)) ()
emitEffect f = tell $ SideEffects mempty (fromList [f])
runReducer :: ReducerM s c e -> s -> c -> (s, [e], [Effect c])
runReducer reducer s cmd =
let ((_, nextState), sideEffects) = runWriter (runStateT (reducer cmd) s)
in (nextState, toList $ outEvents sideEffects, toList $ outEffects sideEffects)classDiagram
class Feature~State, Command, Event~ {
<<interface>>
+StateFlow~State~ state
+Flow~Event~ events
+execute(command: Command): suspend*
+close()
}
class ReducerFeature~State, Command, Event~ {
-CoroutineScope scope
-Reducer~State, Command, Event~ reducer
-Channel~Command~ _commands
-MutableSharedFlow~Event~ _events
-Atomic~Map~ Any, Job~~ jobs
-processEffect(effect: Effect)
-launchManaged(key: Any, block)
-cancelJob(key: Any)
}
class Reducer~State, Command, Event~ {
<<interface>>
+reduce(state: State, command: Command): Transition~State, Event~*
+transition(state: State): Transition~State, Event~
}
class Transition~State, Event~ {
+State state
+List~Event~ events
+List~Effect~ effects
+event(event): Transition
+events(vararg events): Transition
+effect(effect): Transition
+effects(vararg effects): Transition
}
class Effect {
<<sealedinterface>>
}
class Effect_Stream~Command~ {
+Any key
+Flow~Command~ flow
+Strategy strategy
+((Throwable) -> Command) ? fallback
}
class Effect_Action~Command~ {
+Any key
+(() -> Command) block
+((Throwable) -> Command) ? fallback
}
class Effect_Cancel {
+Any key
}
Effect <|.. Effect_Stream
Effect <|.. Effect_Action
Effect <|.. Effect_Cancel
Feature <|.. ReducerFeature
ReducerFeature --> Reducer
ReducerFeature --> Transition
ReducerFeature ..> Effect
Reducer --> TransitionAn immutable data structure representing the current state of a component.
A message sent to the feature to request a state modification or trigger an effect.
A one-time notification for external subscribers (e.g. navigation actions, toasts, analytics).
A declarative description of an asynchronous side effect executed outside the reduction function.
Stream<out Command>: Subscribes to an external Flow<Command> with automated lifecycle management.Action<out Command>: Executes a one-off suspendable task with optional error fallback.Cancel: Immediately terminates an ongoing Action or Stream by its key.Sequential: Emits and processes commands sequentially (flow.collect).Restart: Cancels previous processing when a new emission occurs (flow.collectLatest).A lifecycle-aware container managing state emission, event propagation, and effect execution. It extendsAutoCloseable.
// Instantiation
val feature = Feature<SomeState, SomeCommand, SomeEvent>(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = SomeReducer(),
SomeCommand.LoadInitialData
)
// Delegation pattern (recommended)
class SomeFeature(reducer: SomeReducer) : Feature<SomeState, SomeCommand, SomeEvent> by Feature(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
SomeCommand.LoadInitialData
)A stateless functional interface:
interface Reducer<State, Command, Event in out> {
fun reduce(state: State, command: Command): Transition<State, Event>
fun transition(state: State) = Transition<State, Event>(state = state, events = emptyList(), effects = emptyList())
}transition(state): Initializes a Transition with empty event and effect lists.action(key, fallback) { ... }: Creates an Effect.Action.stream(key, flow, strategy, fallback): Creates an Effect.Stream.cancel(key): Creates an Effect.Cancel.class SomeReducer : Reducer<SomeState, SomeCommand, SomeEvent> {
override fun reduce(state: SomeState, command: SomeCommand) = when (command) {
is SomeCommand.HandleError ->
transition(state).event(SomeEvent.NotifyError(command.throwable.message.orEmpty()))
is SomeCommand.LoadData ->
transition(state).effect(
stream(
key = "data_stream",
flow = observeDataUseCase().map(SomeCommand::UpdateData),
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.UpdateData ->
transition(state.copy(data = command.data)).event(SomeEvent.ShowSuccess)
is SomeCommand.PerformAction ->
transition(state).effect(
action(
key = "save_action",
block = {
val result = repository.save()
SomeCommand.OnSaved(result)
},
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.Stop ->
transition(state).effect(cancel(key = "data_stream"))
}
}Let $S$ be the set of states, $C$ the set of commands, $Ev$ the set of events, and $Ef$ the set of effects.
The state transition function is defined as: $$R: S \times C \rightarrow S \times \mathcal{P} (Ev) \times \mathcal{P} (Ef)$$
Where: $$R (s, c) = (s', ev, ef)$$
For convenience, we denote the state projection as: $$s \cdot c = \pi_S (R (s, c))$$
Let $C^*$ be the free monoid of command sequences under concatenation ($\cdot$), with the empty sequence $\varepsilon$as identity: $$s \cdot \varepsilon = s$$ $$s \cdot (c_1 \cdot c_2) = (s \cdot c_1) \cdot c_2$$
Extending $R$ inductively over a sequence of commands $W = [c_1, c_2, \dots, c_n] \in C^$: $$R^(s, \varepsilon) = (s, \emptyset, \emptyset)$$ $$R^(s, [c_1]) = R (s, c_1) = (s_1, ev_1, ef_1)$$ $$R^(s, [c_1, c_2]) = (s_2, ev_1 \cup ev_2, ef_1 \cup ef_2) \quad \text{where } R (s_1, c_2) = (s_2, ev_2, ef_2)$$
By monoid action properties, grouping of consecutive sub-sequences is associative: $$R^(R_S^(s, W_1), W_2) = R^*(s, W_1 \cdot W_2)$$
This guarantees that folding commands in batches or sequentially produces the identical final state and equivalent accumulated collections of events and effects.
In general state management, commands are non-commutative (e.g. SetText("A") followed by Clear is not equivalent to
Clear followed by SetText("A")).
However, two commands $c_1, c_2 \in C$ are commutative if and only if their state transitions commute and their generated outputs are order-independent: $$s \cdot c_1 \cdot c_2 = s \cdot c_2 \cdot c_1$$ $$\pi_{Ev, Ef} (R^(s, [c_1, c_2])) = \pi_{Ev, Ef} (R^(s, [c_2, c_1]))$$
When this condition is satisfied: $$R^(s, [c_1, c_2]) = R^(s, [c_2, c_1])$$
The state transition system forms a deterministic monoid action on state $S$, guaranteeing:
Feature and Reducer eliminate monolithic controllers and mutable model state.Reducer is a pure function, keeping business logic strictly deterministic.Feature
transitions can only be triggered via formal Command dispatch.Event) from asynchronous work (Effect).Effect.Stream alongside single-shot
Effect.Action.Feature isolates internal state and processes commands sequentially via a Kotlin
Channel.Reduce & Conquer
concentrates all logic in a single pure reduction contract.graph LR
subgraph "Presentation Layer"
View["View (Compose)"] --> Feature["Feature"]
Feature["Feature"] --> Reducer["Reducer"]
end
subgraph "Domain Layer"
Reducer --> UseCase["Use Case"]
UseCase --> Entity["Entity"]
end
subgraph "Infrastructure Layer"
UseCase --> Repository["Repository"]
Repository --> External["Network / DB / Bluetooth"]
enddata class User(val id: String, val name: String)
data class UserState(val users: List<User> = emptyList())
sealed interface UserCommand {
data object LoadUsers : UserCommand
data class UpdateUsers(val users: List<User>) : UserCommand
data class HandleError(val throwable: Throwable) : UserCommand
}
sealed interface UserEvent {
data class NotifyError(val message: String) : UserEvent
data class ShowSuccess(val message: String) : UserEvent
}
class UserReducer(
private val observeUsersUseCase: ObserveUsersUseCase
) : Reducer<UserState, UserCommand, UserEvent> {
override fun reduce(state: UserState, command: UserCommand) = when (command) {
UserCommand.LoadUsers -> transition(state).effect(
stream(
key = "users_stream",
flow = observeUsersUseCase().map(UserCommand::UpdateUsers),
fallback = { throwable -> UserCommand.HandleError(throwable) }
)
)
is UserCommand.UpdateUsers ->
transition(state.copy(users = command.users)).event(UserEvent.ShowSuccess("Users updated"))
is UserCommand.HandleError ->
transition(state).event(UserEvent.NotifyError("Failed: ${command.throwable.message}"))
}
}
class UserFeature(reducer: UserReducer) : Feature<UserState, UserCommand, UserEvent> by Feature(
initialState = UserState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
UserCommand.LoadUsers
)
@Composable
fun UserScreen(feature: UserFeature) {
val state by feature.state.collectAsState()
LaunchedEffect(Unit) {
feature.events.collect { event ->
when (event) {
is UserEvent.NotifyError -> showSnackbar(event.message)
is UserEvent.ShowSuccess -> showSnackbar(event.message)
}
}
}
}Because business logic is encapsulated in pure functions, reducers can be verified synchronously without mocking coroutine scopes:
@Test
fun `load data produces stream effect`() {
val reducer = UserReducer(observeUsersUseCase)
val initialState = UserState()
val transition = reducer.reduce(initialState, UserCommand.LoadUsers)
assertEquals(initialState, transition.state)
assertEquals(0, transition.events.size)
assertEquals(1, transition.effects.size)
assertIs<Effect.Stream<*>>(transition.effects.first())
}A cross-platform Pokédex application demonstrating Reduce & Conquer in practice.
This project is licensed under the MIT License - see the LICENSE file for details.