Reduce. Conquer. Repeat.
- About
- Installation
- Core Principles
- Mathematical Core
- Universal Architecture Components
- Universal Implementation
- Overview
- Mathematical Foundation
- Comparison with popular patterns
- Clean Architecture
- Example (Proof of Concept)
- More examples
- License
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])
- Sequential Composability: State transitions over a stream of commands form a well-defined sequential fold.
- Side-Effect Purity: Side effects are descriptions of work (
Action,Stream,Cancel), not executions. - Separation of Concerns: State updates, external notifications (
Event), and deferred work (Effect) do not pollute one another.
The pattern defines four core abstractions:
- State: The immutable representation of component state at any given point in time.
- Command: An intention to trigger a state transition or schedule an effect.
- Event: A one-time notification emitted during a transition (for UI side-channels like Snackbars, Toasts, or navigation).
- Effect: A structured asynchronous instruction (
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:
TypeScript
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};
};Swift
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)
}
}
}
}
}Dart
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 { /* ... */ }
}Rust
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;
}
}
}Go
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)
}
}
}Haskell
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 --> Transition
An 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 externalFlow<Command>with automated lifecycle management.Action<out Command>: Executes a one-off suspendable task with optional error fallback.Cancel: Immediately terminates an ongoingActionorStreamby 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 aTransitionwith empty event and effect lists.action(key, fallback) { ... }: Creates anEffect.Action.stream(key, flow, strategy, fallback): Creates anEffect.Stream.cancel(key): Creates anEffect.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
The state transition function is defined as:
Where:
For convenience, we denote the state projection as:
Let
Extending
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
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
-
Reproducibility: Given an initial state
$s_0$ and sequence$W$ , the resulting state$s_n$ is strictly deterministic. - Testability: The core logic is verified without mocking asynchronous schedulers or UI frameworks.
- Contrast:
FeatureandReducereliminate monolithic controllers and mutable model state. - Advantage:
Reduceris a pure function, keeping business logic strictly deterministic.
- Contrast: Unlike ViewModel where multiple methods can freely mutate state fields concurrently,
Featuretransitions can only be triggered via formalCommanddispatch. - Advantage: Eliminates state race conditions and non-linear mutation chains.
- Contrast: Strictly decouples one-shot UI notifications (
Event) from asynchronous work (Effect). - Advantage: Side effects do not require complex external middleware (thunks/sagas); cancellation and reactive flows are natively handled through Kotlin Coroutines and typed keys.
- Contrast: Native JVM/Kotlin adaptation designed specifically for Kotlin Coroutines, Kotlin Flow, and Compose.
- Advantage: Direct support for long-running reactive flows via
Effect.Streamalongside single-shotEffect.Action.
- Contrast: Like an Actor,
Featureisolates internal state and processes commands sequentially via a KotlinChannel. - Advantage: Unlike traditional actors that scatter transition rules across message handlers,
Reduce & Conquerconcentrates 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"]
end
data 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.
- Jetpack Compose Multiplatform
- Kotlin Coroutines & Flow
- Kotlinx AtomicFU
- Kotlinx Datetime
- Kotlinx Serialization
- Koin Dependency Injection
- Kotlin Multiplatform UUID
- haskcore - A lightweight standalone Haskell IDE powered by Compose Desktop
- GrokViewer - Application for viewing, filtering, and exporting Grok AI archives
- StarsNoMore - GitHub repositories statistics and traffic summary tool
- Klarity - Compose Desktop media player
This project is licensed under the MIT License - see the LICENSE file for details.
