← Nick Achtien

Server-driven UI in Jetpack Compose

A Compose rendering layer driven by a versioned backend schema, so screens are composed server-side from components the client already ships.

Tools For Humanity · 2025

Context

World App is a financial application with over 10 million downloads, serving international markets. It needed continuous product iteration on transaction history, but every UI change rode the app-store release train, and because old client versions never disappear, each change also had to work on every version already in the field. Product velocity was capped by the slowest release cycle rather than by the work itself.

Why the obvious approach failed

The obvious remedies each solve a different problem than the one we had. Shortening the release train reduces latency but does not remove the store from the loop, and it does nothing about clients that never update. Feature flags toggle behaviour that already shipped — they let you turn a screen off, not compose a new one. WebViews decouple delivery completely, but for financial data they trade away exactly what matters: native input handling, native accessibility, and the performance characteristics users read as trustworthiness in an app that shows them their money. What was needed was native rendering with server-side composition, which is a contract problem rather than a delivery problem.

The mechanism

The server describes a screen as versioned, typed schema; the client owns rendering. The contract was co-designed with backend engineering rather than handed over, because the interesting tension in server-driven UI is between rendering flexibility and client simplicity, and it can only be resolved by both sides at once.

A component registry maps schema nodes to a vetted set of composables. Product teams compose transaction-detail layouts from parts the client team has already hardened, so new screens inherit existing accessibility, theming, and formatting behaviour rather than reimplementing it.

For server-driven UI, the schema is the screenshot — the payload is the honest description of what the system does, more so than any rendering of one particular screen.

I set the architectural direction for the same pattern on iOS. One contract, two platforms: the composition rules live in one place and both clients agree on what a screen means.

The hard part

Version skew, and money. A schema that introduces a component type an older client has never heard of must not crash that client — and in a financial app, must not silently render an incomplete screen either, because a transaction list missing a row is worse than a transaction list that fails honestly. The versioning strategy makes unsupported schema fall back to native rendering deterministically, so the failure mode is a known-good screen rather than an unknown one. Layered on top: the values being rendered are balances, amounts and timestamps across international markets, where currency formatting and locale conventions are correctness requirements rather than presentation preferences.

Results

Product teams ship UI changes without an app release, and feature turnaround moved from a multi-day store cycle to a same-day deploy. The honest caveats: this measures the schema-driven surface, not every change in the app — work that needs new component types still ships on the normal release train, by design. The pattern itself is industry-standard rather than novel; Airbnb and DoorDash have both published comparable systems, and DoorDash has publicly reported delivering components in under a day.

What I'd do differently

[NICK TO CONFIRM] Candidate: invest earlier in tooling that previews a schema payload against a specific client version, so the version-skew question is answerable before deploy rather than reasoned about.

Role Staff Android Engineer

Period Jan 2025 – Aug 2025

Stack Kotlin · Jetpack Compose · Server-driven UI · Schema versioning · REST

Next — One Kotlin Multiplatform core for iOS and Android