Skip to content

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.

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.

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.

DimensionMVVMSynapse
State ownershipViewModel owns screen state, survives config changesNode owns screen state, survives config changes via @Serializable + rememberSaveable
Cross-feature commsAd hoc — shared repositories, singleton SharedFlows, callbacks through navigationBuilt-in — Broadcast/ ListenFor for state, Trigger/ ReactTo for reactions, routed through the SwitchBoard
MiddlewareManual — wrap repository calls, add OkHttp interceptors on the network edgeFirst-class — 6 intercept points cover state, reactions, and requests with read / transform / full control
Data fetchingRepository pattern — ViewModel calls a repository, maps to UI stateDataImpulse Provider with automatic dedup, DataState lifecycle, and KSP wiring
TestabilityMock the repository, test the ViewModel in isolationTest through the real SwitchBoard with interceptor capture. If A and B both work, A+B works
EcosystemMassive — every tutorial, library, and sample assumes itSmall — Synapse-specific, fewer community resources
Learning curveLow — most Android developers already know itModerate — three channels, interceptors, and the bus model require ramp-up
BoilerplateLow to moderate — ViewModel + UI, sometimes a repository layerLow — CreateContext + Node replaces ViewModel entirely; KSP generates provider wiring
Static traceabilityHigh — IDE “find usages” on a ViewModel method shows callersPlugin-assisted — Synapse Navigator provides gutter icons and extended Find Usages
Process deathSavedStateHandle provides key-value persistence scoped to the ViewModel@Serializable + rememberSaveable handles Node state; Coordinator state is not automatically preserved
Hiring / onboardingNear-zero ramp-up — industry standardRequires 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.

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.

DimensionMVISynapse
State modelOne state per screen, updated exclusively through a reducerOne state per Node, updated via update { } — similar in practice
Intent routingSealed class → when branch in the reducerTyped impulses → individual ReactTo / ListenFor registrations
Side effectsFramework-specific (SideEffect, Label, etc.) — often the hardest part Trigger/ Broadcast — the same API as everything else
MiddlewareVaries — some frameworks offer it, many don’tBuilt-in at all 6 intercept points
Multi-screen coordinationDifficult — one store per screen; cross-screen state needs a shared storeNative — the SwitchBoard is the shared bus
PredictabilityHigh — pure reducer, immutable state, auditable transitionsHigh — update is a reducer, state is immutable, interceptors are observable
BoilerplateHigh — Action, Reducer, SideEffect, ViewModel/Store per screenLower — impulse data classes replace Action sealed classes, no reducer boilerplate
Time-travel debuggingSupported by some implementationsNot built-in — use interceptors to log transitions
Event exhaustivenessCompiler-enforced — sealed when on IntentPlugin-assisted — Synapse Navigator flags unconnected channels as IDE inspections
Transition auditabilityAll transitions visible in one reducer functionTransitions 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.

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.

DimensionRIBsSynapse
TopologyTree — parent RIBs attach/detach children, data flows through the treeFlat — all components communicate through the SwitchBoard, no hierarchy
ScopingExplicit — each RIB has its own DI scope; child lifecycle tied to parentImplicit — Node lifecycle tied to Compose; Coordinator tied to LifecycleOwner
CommunicationListener interfaces between parent and child (typed but coupled to tree structure)Impulses through the bus (typed but decoupled from any structure)
NavigationRouter-driven — Routers attach/detach child RIBsImpulse-driven — Trigger(NavigateTo(...)) consumed by a navigation coordinator
Compose supportRetrofit — RIBs predates Compose, integration exists but isn’t nativeNative — CreateContext/Node are Compose-first
BoilerplateVery high — Router + Interactor + Builder + (View) per featureLow — Node or Coordinator per feature, no ceremony
Team scalabilityExcellent — strict boundaries prevent teams from coupling to each otherExcellent — zero coupling, impulse types are the only shared contract
Structural enforcementEnforced by the tree — a RIB can only talk to its parent and childrenConvention-based — any component can Trigger any impulse; discipline + namespacing
Lifecycle scopingHierarchical — child lifecycle strictly bounded by parent; cleanup is deterministicFlat — Node is tied to composition, Coordinator to LifecycleOwner; no parent-child nesting
Production track recordProven 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.

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.

DimensionCircuitSynapse
State ownershipPresenter produces CircuitUiState, consumed by CircuitContentNode holds state, updated via update { }, rendered in the Node body
Event modelUI emits CircuitUiEvent, Presenter handles itUI fires Trigger(impulse), a Node or Coordinator reacts via ReactTo
NavigationState-driven — Navigator pushes/pops screens as state transitions, testable as pure stateImpulse-driven — Trigger(NavigateTo(...)); flexible but less type-safe
MiddlewareLimited — no built-in middleware layerFirst-class — 6 intercept points, read / transform / full
Data fetchingStandard — Presenter calls repositories, maps to stateDataImpulse Provider with dedup and DataState lifecycle
MultiplatformStrong — designed for KMP from the start, shipping on iOS/Desktop at SlackYes on paper — dependencies are multiplatform; not yet validated on non-Android targets
Cross-feature commsManual — shared Presenters or injected dependenciesBuilt-in — state broadcasts and reaction impulses route through the SwitchBoard
TestingPresenter tests with fake UIs, snapshot testingReal SwitchBoard tests with interceptor capture — no mocking
UI / logic separationStrict — Presenter is a pure function from events to state; UI is a pure function from stateBlended — Node body mixes state management with UI rendering; logic can live in the tree
Navigation testabilityHigh — navigation is state, so back-stack assertions are state assertionsLower — 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.

MVVMMVIRIBsCircuitSynapse
Cross-feature coordinationAd hocAd hocTree-basedAd hocBus-native
MiddlewareNoneVariesNoneNone6 intercept points
Data fetching abstractionRepositoryRepositoryServiceRepositoryProvider + DataState
Compose-nativePartialPartialNoYesYes
MultiplatformYes (since lifecycle 2.8)SomeNoYes (proven)Yes (untested on non-Android)
Ecosystem maturityDominantMatureMatureGrowingEarly
BoilerplateLowHighVery highLowLow
Testing modelMock dependenciesMock storeMock interactorFake UIReal bus, no mocks
Learning curveLowModerateHighLowModerate
Static traceabilityIDE call graphSealed whenTree structurePresenter/UI boundaryPlugin-assisted (Synapse Navigator)
Event exhaustivenessN/A (method calls)Compiler-enforcedCompiler-enforcedCompiler-enforcedPlugin-assisted
Structural enforcementModerate (DI scopes)LowStrict (tree hierarchy)Moderate (Presenter boundary)Convention-based (impulse namespacing)
IDE / toolingFirst-classModerateLowModerateSynapse Navigator plugin
UI / logic separationClear (VM / Composable)Clear (Reducer / UI)Clear (Interactor / View)Strict (Presenter / UI)Blended (Node body mixes both)
Production track recordUbiquitousWidespreadUber-scaleSlack-scaleEarly

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.