
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 app architecture.
This repository contains a reference implementation and comprehensive example demonstrating how the pattern integrates with Clean Architecture, using a Pokédex application built with Compose Multiplatform as the demonstration vehicle.
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—are encapsulated in Effects. The reduction core remains pure, while effects are managed through a structured system.
Data flows in a single direction: Command → Reducer → (State, Events, Effects) → Command. This eliminates circular dependencies and makes system behavior traceable.
Every architectural decision is backed by formal proofs. The system's properties aren't just conventions—they're mathematically guaranteed.
At the heart of the pattern lies a single, pure function:
reduce :: (State, Command) → (State, [Event], [Effect])
This function must satisfy algebraic laws:
Associativity: Sequential command application is order-independent
Commutativity (where applicable): Parallel commands produce consistent results
Idempotency: Repeated commands yield identical outcomes
These properties are formally proven and enable:
Deterministic state management
Formal verification capabilities
Predictable system behavior
The pattern defines four core abstractions that work together to form a complete system:
State The immutable representation of system state at any point in time. States are idempotent and deterministic—replaying the same commands always produces identical states.
Command An intention to change the system. Commands are declarative messages that describe "what should happen" without specifying implementation details.
Event A notification that something occurred. Events are fire-and-forget messages emitted during state transitions, useful for auditing, logging, or external notifications.
Effect A structured side operation. Effects manage asynchronous work, reactive streams, or external interactions while maintaining purity in the core reduction logic.
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"]While this repository demonstrates the pattern in Kotlin with Compose Multiplatform, Reduce & Conquer is fundamentally platform-agnostic. The same mathematical principles apply across any language, framework, or domain.
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 => {
});
};
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 {
}
}
class FeatureBuilder<State, Command, Event> extends StatelessWidget {
final Feature<State, Command, Event> feature;
final Widget Function(BuildContext context, State state, void Function(Command) execute) builder;
const FeatureBuilder({required this.feature, required this.builder});
@override
Widget build(BuildContext context) {
return StreamBuilder<State>(
stream: feature.state,
builder: (context, snapshot) {
final state = snapshot.data ?? feature.initialState;
return builder(context, state, feature.execute);
},
);
}
}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])
counterReducer :: ReducerM Int String String
counterReducer "increment" = do
modify (+1)
emitEvent "Incremented"
counterReducer "reset" = do
put 0
emitEvent "ResetDone"
emitEffect (Cancel "timer")
counterReducer _ = return ()
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)Reduce & Conquer is a universal architectural pattern based on a rigorous mathematical model where state transitions are governed by pure reduction functions:
$$R: S \times C \rightarrow (S, Ev, Ef)$$
This establishes a predictable, testable, and composable foundation for software systems across any platform, framework, or domain.
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)
+action(key, fallback, block)
+stream(key, flow, strategy, fallback)
+cancel(key)
}
class Transition~State, Event~ {
+State state
+List~Event~ events
+List~Effect~ effects
}
class Effect {
<<sealed interface>>
}
class Effect_Stream~Command~ {
+Any key
+Strategy strategy
+Flow~Command~ flow
+((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 --> Event
ReducerFeature --> Effect
Reducer --> Transition
[!TIP] The idempotent nature of deterministic state allows you to implement functionality such as rolling back the state to a previous version.
A class or object that describes the current state of the functional unit (or component).
A class or object that describes an action that entails updating state and/or producing effects.
An interface that describes events caused by the execution of a command and the reduction of the state.
[!IMPORTANT] Effects are the primary mechanism for managing side effects in version 3.0.0. They handle reactive streams and deferred operations while maintaining separation from UI events.
An interface that describes side effects for managing asynchronous operations and reactive streams.
Stream<out Command> - for collecting reactive data streams with automatic lifecycle managementAction<out Command> - for executing deferred operations with error handlingCancel - for cancelling ongoing operations by keyThe Effect.Stream supports two execution strategies to manage how data flows are collected within theReducerFeature:
Sequential: Processes emitted commands one by one. It uses the standard collect mechanism, ensuring that every
command from the flow is executed in the order it was received.
Restart: Implements "latest-only" logic using collectLatest. If a new emission occurs before the previous command
processing is complete, the previous job is cancelled and replaced by the new one.
An interface that takes three type parameters: State, Command, and Event.
A functional unit or aggregate of business logic within isolated functionality.
[!NOTE] The
Featureinterface extendsAutoCloseable, allowing it to be used in try-with-resources patterns (Java) oruseblocks (Kotlin) for automatic resource management.
state: A read-only state flow that exposes the current state.events: A flow that exposes the events produced by the feature.suspend execute(command): Submits a command for processing.close(): Terminates all operations and cleans up resources.invoke(initialState: State, scope: CoroutineScope, reducer: Reducer<State, Command, Event>, vararg initialCommands: Command):
Returns a standard implementation of the Feature.
class SomeFeature(reducer: SomeReducer) : Feature<SomeState, SomeCommand, SomeEvent> by Feature(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
SomeCommand.LoadInitialData
)val feature = Feature<SomeState, SomeCommand, SomeEvent>(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = SomeReducer(),
SomeCommand.LoadInitialData
)The ReducerFeature class provides a reference implementation:
Channel for command processing with UNLIMITED capacityscan operatorA functional interface that takes three generic type parameters: State, Command, and Event.
A stateless component responsible for reducing the input command to a new state, events, and effects.
reduce(state, command): Reduces the State with the given Command and returns a Transition containing new
state, events, and effects.
transition(state): Creates a Transition with the given State and empty
events/effects lists. This is a convenience method for creating initial transitions.
action(key, fallback, block): Creates an Effect.Action. Useful for one-off asynchronous operations like a single
API call or database transaction.
stream(key, flow, strategy, fallback): Creates an Effect.Stream. It allows the feature to react to long-running
data streams with a specific Strategy.
cancel(key): Creates an Effect.Cancel. Immediately terminates any ongoing job (Action or Stream) associated
with the provided key.
A data class that represents a state transition.
state: The new State.
events: A list of Events emitted during the transition (for notifications).
effects: A list of Effects produced during the transition (for side effect management).
event(event): Adds a single event to the transition
events(vararg events): Adds multiple events to the transition
effect(effect): Adds a single side effect to be processed
effects(vararg effects): Adds multiple side effects to the transition
The effect system provides structured side effect management:
class SomeReducer : Reducer<SomeState, SomeCommand, SomeEvent> {
override fun reduce(state: SomeState, command: SomeCommand) = when (command) {
is SomeCommand.HandleError -> transition(state).event(SomeEvent.NotifyError("Error: ${command.throwable.message}"))
is SomeCommand.LoadData -> transition(state).effect(
stream(
key = "data_stream",
flow = observeUsers.execute(input = Unit).map(SomeCommand::UpdateData),
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.UpdateData -> transition(state.copy(data = command.data)).event(SomeEvent.ShowSuccess("Data updated"))
is SomeCommand.PerformAction -> transition(state).effect(
action(
key = "deferred_action",
block = {
val result = performComplexCalculation()
SomeCommand.OnResult(result)
},
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.Stop -> transition(state).effect(cancel(key = "data_stream"))
}
}Let $S$ be the set of states, $C$ be the set of commands, $Ev$ be the set of events, and $Ef$ be the set of effects.
We define a function $R: S \times C \rightarrow (S, Ev, Ef)$, which represents the reduction function that takes a state and a command as input and returns a new state, a set of events, and a set of effects.
The function $R$ satisfies the following properties:
Associativity: For all $s \in S$, $c_1, c_2 \in C$, we have: $$R(R(s, c_1), c_2) = R(s, [c_1, c_2])$$ where $[c_1, c_2]$ denotes the composition of commands $c_1$ and $c_2$.
Commutativity (under specific conditions): For all $s \in S$, $c_1, c_2 \in C$ such that $c_1 \circ c_2 = c_2 \circ c_1$, we have: $$R(s, c_1) = R(s, c_2)$$
Let $s \in S$, $c_1, c_2 \in C$. We need to show that: $$R(R(s, c_1), c_2) = R(s, c_1 \circ c_2)$$
Apply Command $c_1$: $$R(s, c_1) = (s_1, ev_1, ef_1)$$ where $s_1$ is the new state, $ev_1$ are the events generated, and $ef_1$ are the effects generated by applying $c_1$ to state $s$.
Apply Command $c_2$ to the New State $s_1$: $$R(s_1, c_2) = (s_2, ev_2, ef_2)$$ where $s_2$ is the new state after applying $c_2$ to $s_1$, $ev_2$ are the events generated, and $ef_2$ are the effects generated.
Sequential Application of Commands $c_1$ and $c_2$: $$R(s, c_1 \circ c_2) = (s_2, ev_1 \cup ev_2, ef_1 \cup ef_2)$$ where $c_1 \circ c_2$ denotes applying $c_1$ first, resulting in $s_1$, $ev_1$, and $ef_1$, and then applying $c_2$ to $s_1$, resulting in $s_2$, $ev_2$, and $ef_2$.
Since both $R(R(s, c_1), c_2)$ and $R(s, c_1 \circ c_2)$ yield the same state $s_2$, combined events $ev_1 \cup ev_2$, and combined effects $ef_1 \cup ef_2$, we have: $$R(R(s, c_1), c_2) = R(s, c_1 \circ c_2)$$
This shows that the reduction function satisfies associativity in the context of command composition.
For commutativity under specific conditions where commands are commutative:
Let $s \in S$, $c_1, c_2 \in C$. We need to show that: $$R(s, c_1 \circ c_2) = R(s, c_2 \circ c_1)$$
Apply Command $c_1$ and then $c_2$: $$R(s, c_1) = (s_1, ev_1, ef_1)$$ $$R(s_1, c_2) = (s_2, ev_2, ef_2)$$ where $s_2$ is the state resulting from applying $c_2$ to $s_1$, $ev_2$ are the events generated, and $ef_2$ are the effects generated.
Apply Command $c_2$ and then $c_1$: $$R(s, c_2) = (s_1', ev_1', ef_1')$$ $$R(s_1', c_1) = (s_2', ev_2', ef_2')$$ where $s_2'$ is the state resulting from applying $c_1$ to $s_1'$, $ev_2'$ are the events generated, and $ef_2'$ are the effects generated.
Since $c_1$ and $c_2$ are commutative (i.e., $c_1 \circ c_2 = c_2 \circ c_1$), the states, events, and effects should be the same: $$(s_2, ev_1 \cup ev_2, ef_1 \cup ef_2) = (s_2', ev_1' \cup ev_2', ef_1' \cup ef_2')$$
Thus, we have: $$R(s, c_1 \circ c_2) = R(s, c_2 \circ c_1)$$
This demonstrates the commutativity of the reduction function under the specific condition of commutative commands.
We have successfully proved that the reduction function $R$ satisfies both associativity and commutativity under the given conditions. This ensures that the reduction function behaves predictably and consistently when applying commands in different sequences, which is essential for ensuring the correctness and reliability of the system.
The associativity property ensures that the order in which commands are applied does not affect the final state, events, and effects, while the commutativity property ensures that commands can be applied in any order without affecting the result under specific conditions. These properties provide a solid foundation for ensuring the correctness and reliability of the system, influencing its design and maintenance.
Structure: MVC separates data (Model) from the UI (View), with a Controller mediating between them.
Contrast: In Reduce & Conquer, the Feature and Reducer replace both the Model's state logic and the
Controller's orchestration.
Advantage: Unlike MVC, where controllers often become "massive" and maintain mutable state, the Reducer is a
stateless functional interface, making the logic predictable and easy to test in isolation.
Structure: MVVM relies on data binding and ViewModels to expose state to the UI.
Contrast: While a Feature can be used inside a ViewModel, it is not bound to the UI lifecycle.
Advantage: Reduce & Conquer provides a more rigid contract. MVVM often leads to fragmented logic where
multiple functions mutate the state directly. Here, state can only change through a formal Transition emitted by the
Reducer.
Structure: These patterns use a unidirectional data flow (UDF) where "Intents" or "Actions" update a global or local store.
Contrast: Reduce & Conquer refines the MVI concept by strictly separating Events (one-time notifications) from Effects (asynchronous operations).
Advantage: Redux often struggles with side effect "middleware" (like Thunk or Saga) which can be verbose. In
Reduce & Conquer, the Effect system is built-in and type-safe, using Coroutines and AtomicFU to manage
lifecycle and cancellation without external plugins.
Structure: TEA uses a Model, Update, and Msg cycle, famous for its reliability.
Contrast: Reduce & Conquer is essentially a JVM/Kotlin adaptation of TEA, but optimized for the modern Coroutine-based ecosystem.
Advantage: It introduces the Effect.Stream and Effect.Action types, which allow for native handling of Flow
and suspend functions directly within the architecture, something TEA achieves through "Commands" but with less native
support for reactive streams.
Structure: Actors are independent, concurrent computational units that communicate via asynchronous message passing. Each actor maintains its own private state and processes messages sequentially from its "mailbox."
Contrast: A Feature operates similarly to an Actor: it encapsulates state, processes messages (Command) via a
Channel (serving as the mailbox), and isolates its internal logic. However, Reduce & Conquer provides a more
formal mathematical structure with explicit state transitions and a dedicated side effect system built directly into
the architecture.
Advantage: Unlike traditional Actor frameworks (like Akka) which require complex setup and explicit supervision
hierarchies, Reduce & Conquer is lightweight and leverages Kotlin-native structured concurrency (
Channel.UNLIMITED +
scan operator) to guarantee thread-safe sequential processing. Crucially, while Actors often scatter business logic
across various message handlers, Reduce & Conquer centralizes all transition logic in a pure Reducer function —
following the mathematical model:
$$S \times C \rightarrow (S, Ev, Ef)$$
This makes the system deterministic, easier to debug, and far more testable than the often unpredictable emergent
behavior of raw Actor systems.
Structure: An FSM moves between a finite number of states based on inputs.
Contrast: The Reducer is a mathematical implementation of a State Transition Function:
$S \times C \rightarrow (S, Ev, Ef)$.
Advantage: While many FSM libraries are purely synchronous, Reduce & Conquer is an "Async-FSM." It allows
transitions to trigger long-running operations (Effect.Stream) that eventually feed back into the machine as new
commands, making it ideal for complex protocols like OAuth handshakes or multi-step file processing.
Structure: CQRS separates the models for reading and writing data.
Contrast: The execute(Command) method represents the "Write" side, while the StateFlow represents the "Read"
side (the projection).
Advantage: By using a Reducer, the "Write" logic is centralized and deterministic. The "Read" side is always a
consistent snapshot of the state, ensuring that the UI or any consuming service never sees an intermediate or
corrupted state.
Structure: Systems react to a stream of events.
Contrast: Reduce & Conquer treats every input as a Command and can produce a stream of Event objects for
external subscribers.
Advantage: It provides "Structured Reactivity." Instead of a chaotic web of event listeners, every event is a byproduct of a state transition, providing a clear audit trail of why an event was fired.
Clean Architecture is a software design pattern that separates the application's business logic into layers, each with its own responsibilities.
The main idea is to create a clear separation of concerns, making it easier to maintain, test, and scale the system.
graph LR
subgraph "Presentation Layer"
View["View"] --> Feature["Feature"]
Feature["Feature"] --> Reducer["Reducer"]
end
subgraph "Domain Layer"
UseCase["Use Case"] --> Entity["Entity"]
UseCase["Use Case"] --> Service["Service"]
end
subgraph "Infrastructure Layer"
Service["Service"] --> Repository["Repository"]
Repository["Repository"] --> External["Network / FileSystem / DB"]
end
Reducer --> UseCaseClean Architecture can be represented as follows:
View(
Feature( // Manages Events (notifications) and Effects (side operations)
Reducer( // Produces State, Events, and Effects
UseCase( // Orchestrates specific business scenarios
Service( // Public API and subsystem-specific logic
Repository // Encapsulates data persistence
)
)
)
)
)[!TIP] Organize your package structure by overall model or functionality rather than by purpose. This type of architecture is called "screaming".
Representing the business domain, such as users, products, or orders.
Defining the actions that can be performed on the entities, such as logging in, creating an order, or updating a user.
Handling communication between the application and external systems, such as databases, networks, or file systems.
Providing the necessary infrastructure for the application to run, such as web servers, databases, or operating systems.
As a general-purpose pattern, Reduce & Conquer can be used to implement the Presentation Layer, coordinate
Use Cases, or manage state in other layers of Clean Architecture.
[!NOTE] Although this example uses Jetpack Compose, the Feature can be easily consumed by any Flow-based system (CLI, Ktor server-side, etc.)
Version 3.0.0 simplifies working with data flows through the Effect.Stream type. The ReducerFeature automatically
manages the lifecycle of flow collections.
data class User(val id: String)
interface UserService {
suspend fun observeUsers(): Result<Flow<List<User>>>
}
class ObserveUsers(private val service: UserService) : UseCase<Unit, Flow<List<User>>> {
override suspend fun execute(input: Unit) = service.observeUsers()
}
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 observeUsers: ObserveUsers,
) : Reducer<UserState, UserCommand, UserEvent> {
override fun reduce(state: UserState, command: UserCommand) = when (command) {
UserCommand.LoadUsers -> transition(state).effect(
stream(
key = "users_flow",
flow = observeUsers.execute(input = Unit).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("Error: ${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 -> {
// Show error notification
showSnackbar(event.message)
}
}
}
}
// UI rendering...
}It is assumed that all the important logic is contained in the Reducer, which means that the testing pipeline can be
roughly represented as follows:
val reducer = MyReducer()
val (actualState, actualEvents, actualEffects) = reducer.reduce(initialState, command)
assertEquals(expectedState, actualState)
assertEquals(expectedEvents, actualEvents)
assertEquals(expectedEffects, actualEffects)A cross-platform Pokédex application demonstrating the Reduce & Conquer architecture in practice. This proof of concept validates the mathematical foundations while providing a real-world implementation.
graph TD
subgraph "Use Case"
GetMaxAttributeValue["Get Max Attribute Value"]
GetDailyPokemon["Get Daily Pokemon"]
GetPokemons["Get Pokemons"]
InitializeFilters["Initialize Filters"]
GetFilters["Get Filters"]
SelectFilter["Select Filter"]
UpdateFilter["Update Filter"]
ResetFilter["Reset Filter"]
ResetFilters["Reset Filters"]
CardsReducer["Cards Reducer"]
ChangeSort["Change Sort"]
end
subgraph "Navigation"
NavigationView["Navigation View"] --> NavigationFeature["Navigation Feature"]
NavigationFeature["Navigation Feature"] --> NavigationReducer["Navigation Reducer"]
end
NavigationReducer["Navigation Reducer"] --> NavigationCommand["Navigation Command"]
NavigationCommand["Navigation Command"] --> DailyView["Daily View"]
NavigationCommand["Navigation Command"] --> PokedexView["Pokedex View"]
subgraph "Daily"
DailyView["Daily View"] --> DailyFeature["Daily Feature"]
DailyFeature["Daily Feature"] --> DailyReducer["Daily Reducer"]
end
DailyReducer["Daily Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
DailyReducer["Daily Reducer"] --> GetDailyPokemon["Get Daily Pokemon"]
subgraph "Pokedex"
PokedexView["Pokedex View"] --> PokedexFeature["Pokedex Feature"]
PokedexFeature["Pokedex Feature"] --> PokedexReducer["Pokedex Reducer"]
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
end
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
CardsReducer["Cards Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
CardsReducer["Cards Reducer"] --> GetPokemons["Get Pokemons"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
FilterReducer["Filter Reducer"] --> InitializeFilters["Initialize Filters"]
FilterReducer["Filter Reducer"] --> GetFilters["Get Filters"]
FilterReducer["Filter Reducer"] --> SelectFilter["Select Filter"]
FilterReducer["Filter Reducer"] --> UpdateFilter["Update Filter"]
FilterReducer["Filter Reducer"] --> ResetFilter["Reset Filter"]
FilterReducer["Filter Reducer"] --> ResetFilters["Reset Filters"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
SortReducer["Sort Reducer"] --> CardsReducer["Cards Reducer"]
SortReducer["Sort Reducer"] --> ChangeSort["Change Sort"][!NOTE] The Pokemon card is a double-sided rotating card where
- front side contains name, image and type affiliation
- back side contains name and hexagonal skill graph
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 app architecture.
This repository contains a reference implementation and comprehensive example demonstrating how the pattern integrates with Clean Architecture, using a Pokédex application built with Compose Multiplatform as the demonstration vehicle.
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—are encapsulated in Effects. The reduction core remains pure, while effects are managed through a structured system.
Data flows in a single direction: Command → Reducer → (State, Events, Effects) → Command. This eliminates circular dependencies and makes system behavior traceable.
Every architectural decision is backed by formal proofs. The system's properties aren't just conventions—they're mathematically guaranteed.
At the heart of the pattern lies a single, pure function:
reduce :: (State, Command) → (State, [Event], [Effect])
This function must satisfy algebraic laws:
Associativity: Sequential command application is order-independent
Commutativity (where applicable): Parallel commands produce consistent results
Idempotency: Repeated commands yield identical outcomes
These properties are formally proven and enable:
Deterministic state management
Formal verification capabilities
Predictable system behavior
The pattern defines four core abstractions that work together to form a complete system:
State The immutable representation of system state at any point in time. States are idempotent and deterministic—replaying the same commands always produces identical states.
Command An intention to change the system. Commands are declarative messages that describe "what should happen" without specifying implementation details.
Event A notification that something occurred. Events are fire-and-forget messages emitted during state transitions, useful for auditing, logging, or external notifications.
Effect A structured side operation. Effects manage asynchronous work, reactive streams, or external interactions while maintaining purity in the core reduction logic.
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"]While this repository demonstrates the pattern in Kotlin with Compose Multiplatform, Reduce & Conquer is fundamentally platform-agnostic. The same mathematical principles apply across any language, framework, or domain.
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 => {
});
};
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 {
}
}
class FeatureBuilder<State, Command, Event> extends StatelessWidget {
final Feature<State, Command, Event> feature;
final Widget Function(BuildContext context, State state, void Function(Command) execute) builder;
const FeatureBuilder({required this.feature, required this.builder});
@override
Widget build(BuildContext context) {
return StreamBuilder<State>(
stream: feature.state,
builder: (context, snapshot) {
final state = snapshot.data ?? feature.initialState;
return builder(context, state, feature.execute);
},
);
}
}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])
counterReducer :: ReducerM Int String String
counterReducer "increment" = do
modify (+1)
emitEvent "Incremented"
counterReducer "reset" = do
put 0
emitEvent "ResetDone"
emitEffect (Cancel "timer")
counterReducer _ = return ()
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)Reduce & Conquer is a universal architectural pattern based on a rigorous mathematical model where state transitions are governed by pure reduction functions:
$$R: S \times C \rightarrow (S, Ev, Ef)$$
This establishes a predictable, testable, and composable foundation for software systems across any platform, framework, or domain.
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)
+action(key, fallback, block)
+stream(key, flow, strategy, fallback)
+cancel(key)
}
class Transition~State, Event~ {
+State state
+List~Event~ events
+List~Effect~ effects
}
class Effect {
<<sealed interface>>
}
class Effect_Stream~Command~ {
+Any key
+Strategy strategy
+Flow~Command~ flow
+((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 --> Event
ReducerFeature --> Effect
Reducer --> Transition
[!TIP] The idempotent nature of deterministic state allows you to implement functionality such as rolling back the state to a previous version.
A class or object that describes the current state of the functional unit (or component).
A class or object that describes an action that entails updating state and/or producing effects.
An interface that describes events caused by the execution of a command and the reduction of the state.
[!IMPORTANT] Effects are the primary mechanism for managing side effects in version 3.0.0. They handle reactive streams and deferred operations while maintaining separation from UI events.
An interface that describes side effects for managing asynchronous operations and reactive streams.
Stream<out Command> - for collecting reactive data streams with automatic lifecycle managementAction<out Command> - for executing deferred operations with error handlingCancel - for cancelling ongoing operations by keyThe Effect.Stream supports two execution strategies to manage how data flows are collected within theReducerFeature:
Sequential: Processes emitted commands one by one. It uses the standard collect mechanism, ensuring that every
command from the flow is executed in the order it was received.
Restart: Implements "latest-only" logic using collectLatest. If a new emission occurs before the previous command
processing is complete, the previous job is cancelled and replaced by the new one.
An interface that takes three type parameters: State, Command, and Event.
A functional unit or aggregate of business logic within isolated functionality.
[!NOTE] The
Featureinterface extendsAutoCloseable, allowing it to be used in try-with-resources patterns (Java) oruseblocks (Kotlin) for automatic resource management.
state: A read-only state flow that exposes the current state.events: A flow that exposes the events produced by the feature.suspend execute(command): Submits a command for processing.close(): Terminates all operations and cleans up resources.invoke(initialState: State, scope: CoroutineScope, reducer: Reducer<State, Command, Event>, vararg initialCommands: Command):
Returns a standard implementation of the Feature.
class SomeFeature(reducer: SomeReducer) : Feature<SomeState, SomeCommand, SomeEvent> by Feature(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = reducer,
SomeCommand.LoadInitialData
)val feature = Feature<SomeState, SomeCommand, SomeEvent>(
initialState = SomeState(),
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
reducer = SomeReducer(),
SomeCommand.LoadInitialData
)The ReducerFeature class provides a reference implementation:
Channel for command processing with UNLIMITED capacityscan operatorA functional interface that takes three generic type parameters: State, Command, and Event.
A stateless component responsible for reducing the input command to a new state, events, and effects.
reduce(state, command): Reduces the State with the given Command and returns a Transition containing new
state, events, and effects.
transition(state): Creates a Transition with the given State and empty
events/effects lists. This is a convenience method for creating initial transitions.
action(key, fallback, block): Creates an Effect.Action. Useful for one-off asynchronous operations like a single
API call or database transaction.
stream(key, flow, strategy, fallback): Creates an Effect.Stream. It allows the feature to react to long-running
data streams with a specific Strategy.
cancel(key): Creates an Effect.Cancel. Immediately terminates any ongoing job (Action or Stream) associated
with the provided key.
A data class that represents a state transition.
state: The new State.
events: A list of Events emitted during the transition (for notifications).
effects: A list of Effects produced during the transition (for side effect management).
event(event): Adds a single event to the transition
events(vararg events): Adds multiple events to the transition
effect(effect): Adds a single side effect to be processed
effects(vararg effects): Adds multiple side effects to the transition
The effect system provides structured side effect management:
class SomeReducer : Reducer<SomeState, SomeCommand, SomeEvent> {
override fun reduce(state: SomeState, command: SomeCommand) = when (command) {
is SomeCommand.HandleError -> transition(state).event(SomeEvent.NotifyError("Error: ${command.throwable.message}"))
is SomeCommand.LoadData -> transition(state).effect(
stream(
key = "data_stream",
flow = observeUsers.execute(input = Unit).map(SomeCommand::UpdateData),
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.UpdateData -> transition(state.copy(data = command.data)).event(SomeEvent.ShowSuccess("Data updated"))
is SomeCommand.PerformAction -> transition(state).effect(
action(
key = "deferred_action",
block = {
val result = performComplexCalculation()
SomeCommand.OnResult(result)
},
fallback = { throwable -> SomeCommand.HandleError(throwable) }
)
)
is SomeCommand.Stop -> transition(state).effect(cancel(key = "data_stream"))
}
}Let $S$ be the set of states, $C$ be the set of commands, $Ev$ be the set of events, and $Ef$ be the set of effects.
We define a function $R: S \times C \rightarrow (S, Ev, Ef)$, which represents the reduction function that takes a state and a command as input and returns a new state, a set of events, and a set of effects.
The function $R$ satisfies the following properties:
Associativity: For all $s \in S$, $c_1, c_2 \in C$, we have: $$R(R(s, c_1), c_2) = R(s, [c_1, c_2])$$ where $[c_1, c_2]$ denotes the composition of commands $c_1$ and $c_2$.
Commutativity (under specific conditions): For all $s \in S$, $c_1, c_2 \in C$ such that $c_1 \circ c_2 = c_2 \circ c_1$, we have: $$R(s, c_1) = R(s, c_2)$$
Let $s \in S$, $c_1, c_2 \in C$. We need to show that: $$R(R(s, c_1), c_2) = R(s, c_1 \circ c_2)$$
Apply Command $c_1$: $$R(s, c_1) = (s_1, ev_1, ef_1)$$ where $s_1$ is the new state, $ev_1$ are the events generated, and $ef_1$ are the effects generated by applying $c_1$ to state $s$.
Apply Command $c_2$ to the New State $s_1$: $$R(s_1, c_2) = (s_2, ev_2, ef_2)$$ where $s_2$ is the new state after applying $c_2$ to $s_1$, $ev_2$ are the events generated, and $ef_2$ are the effects generated.
Sequential Application of Commands $c_1$ and $c_2$: $$R(s, c_1 \circ c_2) = (s_2, ev_1 \cup ev_2, ef_1 \cup ef_2)$$ where $c_1 \circ c_2$ denotes applying $c_1$ first, resulting in $s_1$, $ev_1$, and $ef_1$, and then applying $c_2$ to $s_1$, resulting in $s_2$, $ev_2$, and $ef_2$.
Since both $R(R(s, c_1), c_2)$ and $R(s, c_1 \circ c_2)$ yield the same state $s_2$, combined events $ev_1 \cup ev_2$, and combined effects $ef_1 \cup ef_2$, we have: $$R(R(s, c_1), c_2) = R(s, c_1 \circ c_2)$$
This shows that the reduction function satisfies associativity in the context of command composition.
For commutativity under specific conditions where commands are commutative:
Let $s \in S$, $c_1, c_2 \in C$. We need to show that: $$R(s, c_1 \circ c_2) = R(s, c_2 \circ c_1)$$
Apply Command $c_1$ and then $c_2$: $$R(s, c_1) = (s_1, ev_1, ef_1)$$ $$R(s_1, c_2) = (s_2, ev_2, ef_2)$$ where $s_2$ is the state resulting from applying $c_2$ to $s_1$, $ev_2$ are the events generated, and $ef_2$ are the effects generated.
Apply Command $c_2$ and then $c_1$: $$R(s, c_2) = (s_1', ev_1', ef_1')$$ $$R(s_1', c_1) = (s_2', ev_2', ef_2')$$ where $s_2'$ is the state resulting from applying $c_1$ to $s_1'$, $ev_2'$ are the events generated, and $ef_2'$ are the effects generated.
Since $c_1$ and $c_2$ are commutative (i.e., $c_1 \circ c_2 = c_2 \circ c_1$), the states, events, and effects should be the same: $$(s_2, ev_1 \cup ev_2, ef_1 \cup ef_2) = (s_2', ev_1' \cup ev_2', ef_1' \cup ef_2')$$
Thus, we have: $$R(s, c_1 \circ c_2) = R(s, c_2 \circ c_1)$$
This demonstrates the commutativity of the reduction function under the specific condition of commutative commands.
We have successfully proved that the reduction function $R$ satisfies both associativity and commutativity under the given conditions. This ensures that the reduction function behaves predictably and consistently when applying commands in different sequences, which is essential for ensuring the correctness and reliability of the system.
The associativity property ensures that the order in which commands are applied does not affect the final state, events, and effects, while the commutativity property ensures that commands can be applied in any order without affecting the result under specific conditions. These properties provide a solid foundation for ensuring the correctness and reliability of the system, influencing its design and maintenance.
Structure: MVC separates data (Model) from the UI (View), with a Controller mediating between them.
Contrast: In Reduce & Conquer, the Feature and Reducer replace both the Model's state logic and the
Controller's orchestration.
Advantage: Unlike MVC, where controllers often become "massive" and maintain mutable state, the Reducer is a
stateless functional interface, making the logic predictable and easy to test in isolation.
Structure: MVVM relies on data binding and ViewModels to expose state to the UI.
Contrast: While a Feature can be used inside a ViewModel, it is not bound to the UI lifecycle.
Advantage: Reduce & Conquer provides a more rigid contract. MVVM often leads to fragmented logic where
multiple functions mutate the state directly. Here, state can only change through a formal Transition emitted by the
Reducer.
Structure: These patterns use a unidirectional data flow (UDF) where "Intents" or "Actions" update a global or local store.
Contrast: Reduce & Conquer refines the MVI concept by strictly separating Events (one-time notifications) from Effects (asynchronous operations).
Advantage: Redux often struggles with side effect "middleware" (like Thunk or Saga) which can be verbose. In
Reduce & Conquer, the Effect system is built-in and type-safe, using Coroutines and AtomicFU to manage
lifecycle and cancellation without external plugins.
Structure: TEA uses a Model, Update, and Msg cycle, famous for its reliability.
Contrast: Reduce & Conquer is essentially a JVM/Kotlin adaptation of TEA, but optimized for the modern Coroutine-based ecosystem.
Advantage: It introduces the Effect.Stream and Effect.Action types, which allow for native handling of Flow
and suspend functions directly within the architecture, something TEA achieves through "Commands" but with less native
support for reactive streams.
Structure: Actors are independent, concurrent computational units that communicate via asynchronous message passing. Each actor maintains its own private state and processes messages sequentially from its "mailbox."
Contrast: A Feature operates similarly to an Actor: it encapsulates state, processes messages (Command) via a
Channel (serving as the mailbox), and isolates its internal logic. However, Reduce & Conquer provides a more
formal mathematical structure with explicit state transitions and a dedicated side effect system built directly into
the architecture.
Advantage: Unlike traditional Actor frameworks (like Akka) which require complex setup and explicit supervision
hierarchies, Reduce & Conquer is lightweight and leverages Kotlin-native structured concurrency (
Channel.UNLIMITED +
scan operator) to guarantee thread-safe sequential processing. Crucially, while Actors often scatter business logic
across various message handlers, Reduce & Conquer centralizes all transition logic in a pure Reducer function —
following the mathematical model:
$$S \times C \rightarrow (S, Ev, Ef)$$
This makes the system deterministic, easier to debug, and far more testable than the often unpredictable emergent
behavior of raw Actor systems.
Structure: An FSM moves between a finite number of states based on inputs.
Contrast: The Reducer is a mathematical implementation of a State Transition Function:
$S \times C \rightarrow (S, Ev, Ef)$.
Advantage: While many FSM libraries are purely synchronous, Reduce & Conquer is an "Async-FSM." It allows
transitions to trigger long-running operations (Effect.Stream) that eventually feed back into the machine as new
commands, making it ideal for complex protocols like OAuth handshakes or multi-step file processing.
Structure: CQRS separates the models for reading and writing data.
Contrast: The execute(Command) method represents the "Write" side, while the StateFlow represents the "Read"
side (the projection).
Advantage: By using a Reducer, the "Write" logic is centralized and deterministic. The "Read" side is always a
consistent snapshot of the state, ensuring that the UI or any consuming service never sees an intermediate or
corrupted state.
Structure: Systems react to a stream of events.
Contrast: Reduce & Conquer treats every input as a Command and can produce a stream of Event objects for
external subscribers.
Advantage: It provides "Structured Reactivity." Instead of a chaotic web of event listeners, every event is a byproduct of a state transition, providing a clear audit trail of why an event was fired.
Clean Architecture is a software design pattern that separates the application's business logic into layers, each with its own responsibilities.
The main idea is to create a clear separation of concerns, making it easier to maintain, test, and scale the system.
graph LR
subgraph "Presentation Layer"
View["View"] --> Feature["Feature"]
Feature["Feature"] --> Reducer["Reducer"]
end
subgraph "Domain Layer"
UseCase["Use Case"] --> Entity["Entity"]
UseCase["Use Case"] --> Service["Service"]
end
subgraph "Infrastructure Layer"
Service["Service"] --> Repository["Repository"]
Repository["Repository"] --> External["Network / FileSystem / DB"]
end
Reducer --> UseCaseClean Architecture can be represented as follows:
View(
Feature( // Manages Events (notifications) and Effects (side operations)
Reducer( // Produces State, Events, and Effects
UseCase( // Orchestrates specific business scenarios
Service( // Public API and subsystem-specific logic
Repository // Encapsulates data persistence
)
)
)
)
)[!TIP] Organize your package structure by overall model or functionality rather than by purpose. This type of architecture is called "screaming".
Representing the business domain, such as users, products, or orders.
Defining the actions that can be performed on the entities, such as logging in, creating an order, or updating a user.
Handling communication between the application and external systems, such as databases, networks, or file systems.
Providing the necessary infrastructure for the application to run, such as web servers, databases, or operating systems.
As a general-purpose pattern, Reduce & Conquer can be used to implement the Presentation Layer, coordinate
Use Cases, or manage state in other layers of Clean Architecture.
[!NOTE] Although this example uses Jetpack Compose, the Feature can be easily consumed by any Flow-based system (CLI, Ktor server-side, etc.)
Version 3.0.0 simplifies working with data flows through the Effect.Stream type. The ReducerFeature automatically
manages the lifecycle of flow collections.
data class User(val id: String)
interface UserService {
suspend fun observeUsers(): Result<Flow<List<User>>>
}
class ObserveUsers(private val service: UserService) : UseCase<Unit, Flow<List<User>>> {
override suspend fun execute(input: Unit) = service.observeUsers()
}
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 observeUsers: ObserveUsers,
) : Reducer<UserState, UserCommand, UserEvent> {
override fun reduce(state: UserState, command: UserCommand) = when (command) {
UserCommand.LoadUsers -> transition(state).effect(
stream(
key = "users_flow",
flow = observeUsers.execute(input = Unit).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("Error: ${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 -> {
// Show error notification
showSnackbar(event.message)
}
}
}
}
// UI rendering...
}It is assumed that all the important logic is contained in the Reducer, which means that the testing pipeline can be
roughly represented as follows:
val reducer = MyReducer()
val (actualState, actualEvents, actualEffects) = reducer.reduce(initialState, command)
assertEquals(expectedState, actualState)
assertEquals(expectedEvents, actualEvents)
assertEquals(expectedEffects, actualEffects)A cross-platform Pokédex application demonstrating the Reduce & Conquer architecture in practice. This proof of concept validates the mathematical foundations while providing a real-world implementation.
graph TD
subgraph "Use Case"
GetMaxAttributeValue["Get Max Attribute Value"]
GetDailyPokemon["Get Daily Pokemon"]
GetPokemons["Get Pokemons"]
InitializeFilters["Initialize Filters"]
GetFilters["Get Filters"]
SelectFilter["Select Filter"]
UpdateFilter["Update Filter"]
ResetFilter["Reset Filter"]
ResetFilters["Reset Filters"]
CardsReducer["Cards Reducer"]
ChangeSort["Change Sort"]
end
subgraph "Navigation"
NavigationView["Navigation View"] --> NavigationFeature["Navigation Feature"]
NavigationFeature["Navigation Feature"] --> NavigationReducer["Navigation Reducer"]
end
NavigationReducer["Navigation Reducer"] --> NavigationCommand["Navigation Command"]
NavigationCommand["Navigation Command"] --> DailyView["Daily View"]
NavigationCommand["Navigation Command"] --> PokedexView["Pokedex View"]
subgraph "Daily"
DailyView["Daily View"] --> DailyFeature["Daily Feature"]
DailyFeature["Daily Feature"] --> DailyReducer["Daily Reducer"]
end
DailyReducer["Daily Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
DailyReducer["Daily Reducer"] --> GetDailyPokemon["Get Daily Pokemon"]
subgraph "Pokedex"
PokedexView["Pokedex View"] --> PokedexFeature["Pokedex Feature"]
PokedexFeature["Pokedex Feature"] --> PokedexReducer["Pokedex Reducer"]
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
end
PokedexReducer["Pokedex Reducer"] --> CardsReducer["Cards Reducer"]
CardsReducer["Cards Reducer"] --> GetMaxAttributeValue["Get Max Attribute Value"]
CardsReducer["Cards Reducer"] --> GetPokemons["Get Pokemons"]
PokedexReducer["Pokedex Reducer"] --> FilterReducer["Filter Reducer"]
FilterReducer["Filter Reducer"] --> InitializeFilters["Initialize Filters"]
FilterReducer["Filter Reducer"] --> GetFilters["Get Filters"]
FilterReducer["Filter Reducer"] --> SelectFilter["Select Filter"]
FilterReducer["Filter Reducer"] --> UpdateFilter["Update Filter"]
FilterReducer["Filter Reducer"] --> ResetFilter["Reset Filter"]
FilterReducer["Filter Reducer"] --> ResetFilters["Reset Filters"]
PokedexReducer["Pokedex Reducer"] --> SortReducer["Sort Reducer"]
SortReducer["Sort Reducer"] --> CardsReducer["Cards Reducer"]
SortReducer["Sort Reducer"] --> ChangeSort["Change Sort"][!NOTE] The Pokemon card is a double-sided rotating card where
- front side contains name, image and type affiliation
- back side contains name and hexagonal skill graph
This project is licensed under the MIT License - see the LICENSE file for details.