Transmission Library is built on top of Kotlin Coroutines. So concurrency is not a side topic for the library. It is part of the main design.

Signals enter the router. Transformers listen to signals and effects. Handlers can publish more effects, send data, query other transformers, or suspend on checkpoints. This gives us a flexible communication network.

But flexibility has a cost.

When you make more things concurrent, you can also make the system harder to reason about. This is especially dangerous for a library. A user should not need to understand every scheduling detail to trust the output.

Recently, I have been thinking about this tradeoff again:

How can we improve concurrency performance without making Transmission nondeterministic?

This post is not an announcement of a released API. I am experimenting with different API surfaces and implementation strategies on my own fork, apart from the actual library. I want to write down the design pressure before the details disappear from my head.

Performance is not the only goal

It is tempting to look at a concurrent system and think: Can we run more things in parallel?

Usually, yes.

But the better question is:

What is allowed to become parallel without changing the observable result?

If two handlers update unrelated state, running them concurrently might be fine. If two handlers emit ordered data for the same screen, running them concurrently can make the output unstable.

For application code, you can sometimes rely on local knowledge. You know that two operations do not conflict. For library code, assumptions like that are dangerous. The library should provide clear rules.

I don’t want Transmission to become faster by becoming surprising.

A small example

Imagine two transformers listening to the same signal.

sealed interface CounterSignal : Transmission.Signal {
data object Increment : CounterSignal
}
data class CounterData(val value: Int) : Transmission.Data
data class AnalyticsData(val message: String) : Transmission.Data

One transformer updates UI state:

class CounterTransformer : Transformer() {
private val state = dataHolder(CounterData(0))
init {
configure {
onSignal<CounterSignal.Increment> {
state.update { data ->
data.copy(value = data.value + 1)
}
}
}
}
}

Another transformer records some derived information:

class AnalyticsTransformer : Transformer() {
init {
configure {
onSignal<CounterSignal.Increment> {
send(AnalyticsData("Counter incremented"))
}
}
}
}

Both handlers can react to the same signal. It looks harmless to run them concurrently.

The problem starts when the order becomes visible.

router.process(CounterSignal.Increment)
// What should observers receive first?
// CounterData(1)?
// AnalyticsData("Counter incremented")?

Maybe the app does not care. Maybe it does. The library should not accidentally decide this based on coroutine scheduling.

Determinism means boring outputs

For me, deterministic concurrency means the same input sequence should produce the same observable output sequence, assuming the user code itself is deterministic.

That sentence has a lot hidden inside it.

The library cannot make network calls deterministic. It cannot make user code pure. It cannot prevent someone from reading the current time inside a handler.

But the library can avoid adding extra randomness.

If I send these signals:

router.process(CounterSignal.Increment)
router.process(CounterSignal.Increment)
router.process(CounterSignal.Increment)

I don’t want the internal scheduling to decide whether the UI sees:

CounterData(1)
CounterData(2)
CounterData(3)

or something more exciting.

For a UI state library, exciting is not good.

Where parallelism is safer

There are parts of the system where concurrency is less risky.

For example, loading independent transformer sets can be parallel if the final installed order is stable.

val loadedTransformers = loaders
.map { loader -> async { loader.load() } }
.awaitAll()
.flatten()
.sortedBy { it.identity.value }

This is not the exact library code. It shows the idea.

The loading work can happen concurrently, but the result should be normalized before the router starts processing events. We can parallelize the expensive part without letting completion order leak into runtime behavior.

Another safer area is independent validation.

val validationResults = transformers
.map { transformer -> async { validate(transformer) } }
.awaitAll()
validationResults.throwIfInvalid()

Again, validation can be concurrent. But the final decision should be stable.

Where parallelism is dangerous

Handler execution is more sensitive.

This version looks fast:

suspend fun dispatch(signal: Transmission.Signal) = coroutineScope {
matchingHandlers.map { handler ->
async { handler(signal) }
}.awaitAll()
}

But now the handlers race. If they publish effects or send data, output order can depend on which coroutine resumes first.

A more boring version is sequential:

suspend fun dispatch(signal: Transmission.Signal) {
matchingHandlers.forEach { handler ->
handler(signal)
}
}

This is easier to reason about. It is also potentially slower.

The interesting design space is in the middle.

Can we run independent work concurrently, but preserve deterministic publication?

One possible model is to separate execution from emission.

suspend fun dispatch(signal: Transmission.Signal) = coroutineScope {
val results = matchingHandlers
.mapIndexed { index, handler ->
async {
HandlerResult(
index = index,
emissions = handler.collectEmissions(signal),
)
}
}
.awaitAll()
.sortedBy { it.index }
results.forEach { result ->
result.emissions.forEach { emission ->
bus.emit(emission)
}
}
}

This is only a sketch. The actual implementation would be more complicated because handlers can suspend, query, publish effects, and interact with checkpoints.

But the principle is useful:

Run work concurrently when possible. Commit observable results in a stable order.

This is a pattern I want to explore more.

Concurrency boundaries should be explicit

Another option is to let users declare what can be concurrent.

For example:

class SearchTransformer : Transformer() {
init {
configure {
onSignal<SearchSubmitted>(mode = HandlerMode.Sequential) { signal ->
send(SearchData.Loading)
val result = searchRepository.search(signal.query)
send(SearchData.Result(result))
}
}
}
}

And for independent side work:

class LoggingTransformer : Transformer() {
init {
configure {
onSignal<SearchSubmitted>(mode = HandlerMode.Concurrent) { signal ->
analytics.trackSearch(signal.query)
}
}
}
}

I am not saying this should be the final API. It might be too much surface area. But the idea is important. If concurrency changes reasoning, it should be visible in the API.

A hidden optimization is nice until it changes behavior.

Deterministic tests are the contract

The only way to trust this kind of change is to test the ordering contract directly.

A test should not only check that all outputs eventually arrive. It should check that they arrive in the expected order.

@Test
fun emits_counter_updates_in_order() = runTest {
val router = TransmissionRouter {
addTransformerSet(
setOf(CounterTransformer())
)
}
val values = mutableListOf<CounterData>()
val job = launch {
router.streamData<CounterData>().collect { data ->
values += data
}
}
router.process(CounterSignal.Increment)
router.process(CounterSignal.Increment)
router.process(CounterSignal.Increment)
advanceUntilIdle()
assertEquals(
listOf(
CounterData(1),
CounterData(2),
CounterData(3),
),
values,
)
job.cancel()
}

This kind of test is more important than a benchmark in the beginning.

A benchmark tells me whether the change is faster. A deterministic test tells me whether the change is still the same library.

The boring rule I want

The rule I want is simple:

If the same signals are processed against the same transformer graph, Transmission should not add nondeterminism on top of user code.

This does not mean everything must be sequential. It means observable boundaries should be stable.

Some possible rules:

  • Signal processing order should stay stable.
  • Effects should not be reordered accidentally.
  • Router initialization should not depend on loader completion order.
  • Contract lookup should not depend on unstable set iteration when duplicates exist.
  • Concurrent internals should commit results through deterministic boundaries.

That last point is the key.

Concurrency inside the implementation is fine. Concurrency leaking into the user’s mental model is the real problem.

Closing

I still want Transmission to be faster. Performance matters, especially when a screen has many transformers and many events flowing through the system.

But I don’t want performance to come from chaos.

The best version of this work would make the internals more concurrent while making the public behavior more boring. Same inputs, same outputs, same order, fewer surprises.

That is the kind of concurrency I want in a library.

Fast enough to be useful.

Deterministic enough to trust.