How Synapse Compares
Synapse occupies a different point in the design space than mainstream Android architectures. This page is an honest comparison — where Synapse is stronger, where other architectures are stronger, and what each one gives up to get what it gives.
The core difference
Section titled “The core difference”Most architectures organize around who owns state — a ViewModel, a
Store, an Interactor. Synapse organizes around how state moves —
three typed channels with middleware at every stage. Components are
anonymous participants on a shared bus. That choice eliminates the
coordination problem (“how does Screen A tell Coordinator B about Event
C?”) but introduces a different mental model, and the trade-offs flow
downstream from it.
vs MVVM (ViewModel + StateFlow)
Section titled “vs MVVM (ViewModel + StateFlow)”The Google-recommended pattern. A ViewModel holds screen state,
exposes it via StateFlow, and receives UI events as method calls. Hilt
provides the ViewModel to the composable.
| Dimension | MVVM | Synapse |
|---|---|---|
| State ownership | ViewModel owns screen state, survives config changes | Node owns screen state, survives config changes via @Serializable + rememberSaveable |
| Cross-feature comms | Ad hoc — shared repositories, singleton SharedFlows, callbacks through navigation | Built-in — Broadcast/ ListenFor for state, Trigger/ ReactTo for reactions, routed through the SwitchBoard |
| Middleware | Manual — wrap repository calls, add OkHttp interceptors on the network edge | First-class — 6 intercept points cover state, reactions, and requests with read / transform / full control |
| Data fetching | Repository pattern — ViewModel calls a repository, maps to UI state | DataImpulse → Provider with automatic dedup, DataState lifecycle, and KSP wiring |
| Testability | Mock the repository, test the ViewModel in isolation | Test through the real SwitchBoard with interceptor capture. If A and B both work, A+B works |
| Ecosystem | Massive — every tutorial, library, and sample assumes it | Small — Synapse-specific, fewer community resources |
| Learning curve | Low — most Android developers already know it | Moderate — three channels, interceptors, and the bus model require ramp-up |
| Boilerplate | Low to moderate — ViewModel + UI, sometimes a repository layer | Low — CreateContext + Node replaces ViewModel entirely; KSP generates provider wiring |
| Static traceability | High — IDE “find usages” on a ViewModel method shows callers | Plugin-assisted — Synapse Navigator provides gutter icons and extended Find Usages |
| Process death | SavedStateHandle provides key-value persistence scoped to the ViewModel | @Serializable + rememberSaveable handles Node state; Coordinator state is not automatically preserved |
| Hiring / onboarding | Near-zero ramp-up — industry standard | Requires learning a new paradigm |
MVVM’s strength is universality. Every Android hire knows it, every library assumes it, Google’s tooling is built for it. If your app is straightforward CRUD screens with isolated data needs, MVVM is proven and sufficient.
Synapse’s strength is cross-cutting concerns and composition at scale. Auth token injection, analytics, session management, and feature coordination are first-class operations, not afterthoughts bolted onto a repository layer. The zero-coupling guarantee means adding Feature B never requires modifying Feature A.
vs MVI (Redux-style)
Section titled “vs MVI (Redux-style)”MVI patterns (Orbit, MVIKotlin, hand-rolled reducers) use a single
state object, a sealed Intent/Action type, a pure reducer function,
and a side-effect system. State transitions are predictable and
auditable.
| Dimension | MVI | Synapse |
|---|---|---|
| State model | One state per screen, updated exclusively through a reducer | One state per Node, updated via update { } — similar in practice |
| Intent routing | Sealed class → when branch in the reducer | Typed impulses → individual ReactTo / ListenFor registrations |
| Side effects | Framework-specific (SideEffect, Label, etc.) — often the hardest part | Trigger/ Broadcast — the same API as everything else |
| Middleware | Varies — some frameworks offer it, many don’t | Built-in at all 6 intercept points |
| Multi-screen coordination | Difficult — one store per screen; cross-screen state needs a shared store | Native — the SwitchBoard is the shared bus |
| Predictability | High — pure reducer, immutable state, auditable transitions | High — update is a reducer, state is immutable, interceptors are observable |
| Boilerplate | High — Action, Reducer, SideEffect, ViewModel/Store per screen | Lower — impulse data classes replace Action sealed classes, no reducer boilerplate |
| Time-travel debugging | Supported by some implementations | Not built-in — use interceptors to log transitions |
| Event exhaustiveness | Compiler-enforced — sealed when on Intent | Plugin-assisted — Synapse Navigator flags unconnected channels as IDE inspections |
| Transition auditability | All transitions visible in one reducer function | Transitions are spread across ReactTo/ ListenFor/ Request handlers |
MVI’s strength is extreme predictability. If you need an audit trail of every state transition with time-travel debugging, MVI’s pure-reducer model is purpose-built for that. Mature implementations like MVIKotlin have multiplatform support and battle-tested patterns.
Synapse’s strength is that MVI solves the single-screen state problem well but struggles with the multi-screen coordination problem. Synapse treats coordination as the primary concern. The three channels replace the ad-hoc event buses and shared stores that MVI apps accumulate over time.
vs RIBs (Router-Interactor-Builder)
Section titled “vs RIBs (Router-Interactor-Builder)”Uber’s architecture. A tree of Routers (navigation), Interactors (business logic), Builders (DI), and optional Views. Each RIB is a self-contained unit with explicit parent-child relationships.
| Dimension | RIBs | Synapse |
|---|---|---|
| Topology | Tree — parent RIBs attach/detach children, data flows through the tree | Flat — all components communicate through the SwitchBoard, no hierarchy |
| Scoping | Explicit — each RIB has its own DI scope; child lifecycle tied to parent | Implicit — Node lifecycle tied to Compose; Coordinator tied to LifecycleOwner |
| Communication | Listener interfaces between parent and child (typed but coupled to tree structure) | Impulses through the bus (typed but decoupled from any structure) |
| Navigation | Router-driven — Routers attach/detach child RIBs | Impulse-driven — Trigger(NavigateTo(...)) consumed by a navigation coordinator |
| Compose support | Retrofit — RIBs predates Compose, integration exists but isn’t native | Native — CreateContext/Node are Compose-first |
| Boilerplate | Very high — Router + Interactor + Builder + (View) per feature | Low — Node or Coordinator per feature, no ceremony |
| Team scalability | Excellent — strict boundaries prevent teams from coupling to each other | Excellent — zero coupling, impulse types are the only shared contract |
| Structural enforcement | Enforced by the tree — a RIB can only talk to its parent and children | Convention-based — any component can Trigger any impulse; discipline + namespacing |
| Lifecycle scoping | Hierarchical — child lifecycle strictly bounded by parent; cleanup is deterministic | Flat — Node is tied to composition, Coordinator to LifecycleOwner; no parent-child nesting |
| Production track record | Proven at Uber scale (hundreds of engineers, years of production use) | Early — production-validated at smaller scale |
RIBs’ strength is being battle-tested at Uber scale with hundreds of engineers. The tree structure provides clear ownership and scoping that prevents the kind of spaghetti flat architectures risk. If you have 50+ engineers and need enforced structural boundaries, RIBs provides them.
Synapse’s strength is that the flat bus eliminates the tree-navigation problem — what happens when a deeply nested RIB needs to communicate with a distant sibling? Synapse components are structurally independent, so adding a new feature never requires modifying the tree. The trade-off is that you lose explicit scoping; discipline around impulse namespacing replaces structural enforcement.
vs Circuit (Slack)
Section titled “vs Circuit (Slack)”Circuit separates Compose UI from business logic via a Presenter that
produces state and consumes events. Navigation is modeled as state.
It’s Compose-native and multiplatform.
| Dimension | Circuit | Synapse |
|---|---|---|
| State ownership | Presenter produces CircuitUiState, consumed by CircuitContent | Node holds state, updated via update { }, rendered in the Node body |
| Event model | UI emits CircuitUiEvent, Presenter handles it | UI fires Trigger(impulse), a Node or Coordinator reacts via ReactTo |
| Navigation | State-driven — Navigator pushes/pops screens as state transitions, testable as pure state | Impulse-driven — Trigger(NavigateTo(...)); flexible but less type-safe |
| Middleware | Limited — no built-in middleware layer | First-class — 6 intercept points, read / transform / full |
| Data fetching | Standard — Presenter calls repositories, maps to state | DataImpulse → Provider with dedup and DataState lifecycle |
| Multiplatform | Strong — designed for KMP from the start, shipping on iOS/Desktop at Slack | Yes on paper — dependencies are multiplatform; not yet validated on non-Android targets |
| Cross-feature comms | Manual — shared Presenters or injected dependencies | Built-in — state broadcasts and reaction impulses route through the SwitchBoard |
| Testing | Presenter tests with fake UIs, snapshot testing | Real SwitchBoard tests with interceptor capture — no mocking |
| UI / logic separation | Strict — Presenter is a pure function from events to state; UI is a pure function from state | Blended — Node body mixes state management with UI rendering; logic can live in the tree |
| Navigation testability | High — navigation is state, so back-stack assertions are state assertions | Lower — navigation is impulse-driven; testing requires interceptor capture of NavigateTo |
Circuit’s strength is a clean Presenter/UI separation with strong multiplatform support. If you’re building for Android + iOS + Desktop and want a Compose-native architecture with good navigation, Circuit is compelling. Slack’s real-world usage validates it at scale.
Synapse’s strength is that Circuit solves the screen-level problem elegantly but doesn’t address cross-feature coordination or middleware. When your app needs auth token injection across all requests, analytics interception, or session management that spans screens, those concerns need custom solutions in Circuit. In Synapse they’re first-class interceptor registrations.
Summary matrix
Section titled “Summary matrix”| MVVM | MVI | RIBs | Circuit | Synapse | |
|---|---|---|---|---|---|
| Cross-feature coordination | Ad hoc | Ad hoc | Tree-based | Ad hoc | Bus-native |
| Middleware | None | Varies | None | None | 6 intercept points |
| Data fetching abstraction | Repository | Repository | Service | Repository | Provider + DataState |
| Compose-native | Partial | Partial | No | Yes | Yes |
| Multiplatform | Yes (since lifecycle 2.8) | Some | No | Yes (proven) | Yes (untested on non-Android) |
| Ecosystem maturity | Dominant | Mature | Mature | Growing | Early |
| Boilerplate | Low | High | Very high | Low | Low |
| Testing model | Mock dependencies | Mock store | Mock interactor | Fake UI | Real bus, no mocks |
| Learning curve | Low | Moderate | High | Low | Moderate |
| Static traceability | IDE call graph | Sealed when | Tree structure | Presenter/UI boundary | Plugin-assisted (Synapse Navigator) |
| Event exhaustiveness | N/A (method calls) | Compiler-enforced | Compiler-enforced | Compiler-enforced | Plugin-assisted |
| Structural enforcement | Moderate (DI scopes) | Low | Strict (tree hierarchy) | Moderate (Presenter boundary) | Convention-based (impulse namespacing) |
| IDE / tooling | First-class | Moderate | Low | Moderate | Synapse Navigator plugin |
| UI / logic separation | Clear (VM / Composable) | Clear (Reducer / UI) | Clear (Interactor / View) | Strict (Presenter / UI) | Blended (Node body mixes both) |
| Production track record | Ubiquitous | Widespread | Uber-scale | Slack-scale | Early |
Picking a pattern
Section titled “Picking a pattern”No architecture is universally superior. The right choice depends on your team’s size, your app’s coordination complexity, your multiplatform needs, and how much you value convention over control.
- MVVM is the default answer for most Android apps, and that’s not wrong. If you can describe your app as “a handful of screens that each read from a repository,” the extra machinery of anything else is friction without payoff.
- MVI is the right answer when every state transition must be audited — regulated domains, finance, anywhere a state-change timeline is a feature of the product.
- RIBs is the right answer when team size, not feature complexity, is the dominant concern — the tree structure is how you keep fifty engineers from stepping on each other.
- Circuit is the right answer for Compose-first multiplatform apps where screen-level clarity and state-driven navigation matter more than cross-cutting middleware.
- Synapse is built for applications where cross-cutting concerns and inter-feature coordination are the dominant complexity — not just rendering screens from data. Auth, analytics, session management, and feature-to-feature handshakes become first-class operations instead of things you bolt onto a repository layer.
The honest short version: if your app’s hardest problem is “how do I show this data on this screen,” the question isn’t whether Synapse is better than MVVM. If your app’s hardest problem is “how does this event in Feature A reach Feature B without Feature A knowing Feature B exists,” that’s the problem Synapse is built for, and the bus model earns its complexity budget.
- Troubleshooting — common issues, error messages, and how to fix them