Skip to content

SwitchBoard

Every Synapse app has a single SwitchBoard. It is the only shared object in the architecture — every Node, Coordinator, and Provider talks to the rest of the app through it. Nothing else holds a reference to anything else.

SwitchBoard is an interface with three groups of methods:

GroupMethodsPurpose
ProducebroadcastState, triggerImpulse, handleRequestEmit data upstream through interceptors into the appropriate channel flow.
ConsumestateFlow, impulseFlowReturn downstream-intercepted SharedFlows for a given type.
Raw accessgetRawStateFlow, getRawImpulseFlowReturn the backing SharedFlow without downstream interception — for advanced composition where the caller manages interception.
InterceptaddInterceptorRegister an interceptor at a specific intercept point. Returns a Registration handle.

Every method takes a KClass token to identify the type on the bus. Reified extension functions let call sites omit the token:

// These are equivalent:
switchBoard.broadcastState(SessionState::class, session)
switchBoard.broadcastState(session) // reified — infers KClass from T

A convenience extension installs logging interceptors across all six intercept points at once:

switchBoard.addLoggingInterceptors<AnalyticsEvent> { event ->
analytics.track(event)
}

DefaultSwitchBoard is the concrete implementation, designed for constructor injection via Hilt or any DI container.

class DefaultSwitchBoard @Inject constructor(
@SwitchBoardScope scope: CoroutineScope,
providerRegistry: ProviderRegistry,
@SwitchBoardWorkerContext workerContext: CoroutineContext = Dispatchers.IO,
@SwitchBoardStopTimeout stopTimeoutMillis: Long = 3_000,
@SwitchBoardReplayExpiration replayExpirationMillis: Long = 3_000,
) : SwitchBoard
ParameterPurposeDefault
scopeParent CoroutineScope. Canceling it tears down every flow the bus manages.(required)
providerRegistryImmutable registry mapping each DataImpulse type to its provider factory.(required)
workerContextCoroutineContext for provider work.Dispatchers.IO
stopTimeoutMillisHow long a shared flow keeps emitting after its last subscriber disconnects.3000 (3 s)
replayExpirationMillisHow long a replay cache is retained after all subscribers disconnect.3000 (3 s)

The four qualifier annotations (@SwitchBoardScope, @SwitchBoardWorkerContext, @SwitchBoardStopTimeout, @SwitchBoardReplayExpiration) let Hilt distinguish each parameter without ambiguity. See Hilt Integration for the module wiring.

Flows are stored in a ConcurrentHashMap per channel and created lazily on first access — a type that is never broadcast or triggered allocates nothing.

ChannelFlow typeReplayBuffer
StateMutableSharedFlow1DROP_OLDEST
ReactionMutableSharedFlow0extraBufferCapacity = 64, DROP_OLDEST
RequestSharedFlow via ProviderManager1

Downstream flows are shared via SharingStarted.WhileSubscribed:

  • Emission continues for stopTimeoutMillis after the last subscriber disconnects, avoiding unnecessary restarts during brief configuration changes.
  • The replay cache is retained for replayExpirationMillis after emission stops, so a subscriber that reconnects quickly still sees the latest value without a re-fetch.

For tests, call setEagerSharing() before any flow is consumed to switch to SharingStarted.Eagerly, so flows are hot from the start and values are never missed due to timing.

All internal maps are ConcurrentHashMap. All flows are thread-safe by Kotlin coroutines contract. The global logger and trace listener use AtomicReference. Every member of SwitchBoard is safe to call from any coroutine or thread.

The SwitchBoard has no dispose() method. Its lifetime is controlled entirely by the CoroutineScope you inject:

  • App-scoped (typical) — lives for the process. Flows survive configuration changes. State replay works across activity recreations.
  • Test-scopedSynapseTestRule creates a fresh scope per test and cancels it on teardown, so nothing leaks between tests.

When the scope is canceled, all shared flows stop, all active provider jobs are canceled, and all interceptor pipelines shut down. No manual cleanup is needed.

  • Hilt Integration — wiring the switchboard, registry, and coordinators in a real app