Last updated: Aug 23, 2024

Server-driven UI from scratch

Written by Hamish Cundy · 12 minutes read

Server-driven UI from scratch

One of the downsides of mobile app development, compared to backend or web, is the lack of control we have over our apps in the wild. While a web developer can redeploy their site and have the updated version instantly available to every user, mobile developers are reliant on Play/App Store review processes, as well as users choosing to update their apps to the latest version. This greatly slows down rapid product iteration. Server-driven UI allows us to have a greater degree of control of our app’s functionality from afar.

Introduction

Something is broken in production. How do we fix it?

The answer is simple for a web or backend developer: make the fix, test it, and deploy it. Job done. But for those of us in the mobile space, the answer is a bit more complex: We don’t actually control the entire deployment lifecycle!

Once our fix is ready, releasing it to the public is at the whims of Apple, Google and their store review processes. Once past that gauntlet, and published to the store, we have to wait for users to install the new app on their device. Altogether, it can take a week or two for a change to reach the majority of our user base. This greatly gets in the way of being able to rapidly improve and iterate on our product, as well as deal with urgent bugs and crashes in a timely manner.

We do have some tools in our toolkit to help with this problem, such as tactical force update mechanisms, feature toggling, and server-side workarounds through to more strategic choices like hybrid apps. However, being able to quickly deploy new features and bug fixes to our apps in the wild is a capability most mobile developers can only dream of.

One potential solution for this problem is Server-driven UI. At Empower, we’ve recently explored this approach in our new app for the Southeast Asia market. This approach gives us significant control over the functionality of our app, while maintaining native mobile app look-and-feel and performance.

Our situation

Earlier this year, Empower acquired Cashalo, a Filipino lending company. Part of the process of integrating Cashalo into the wider Empower business is building a new mobile app and backend for Cashalo, on Empower’s tech stack.

Initially, we planned to build this using the same techniques that have proven successful in our Empower US app. This stack includes native Android and iOS apps which talk to a .NET backend in a RESTful-ish way and allows for frequent client releases to deploy new features and bug fixes.

However, there had been a desire from both our product team and engineering management to take this (mostly) green-fields opportunity in the Philippines to try something new - a different approach that would give us faster iteration and development speed, and the ability to quickly build and test product experiments to deliver better outcomes for our users and the business.

Approach

From the get-go, we wanted a complete solution for moving client-side logic out of our mobile apps. So rather than the piecemeal approach of making particular screens, parts of screens or flows to be server-driven, we sought an approach that abstracted away the important facets of a mobile app to a remote system:

  • Displaying screens, modals and other UI elements
  • Submitting user input and selections to the backend
  • Navigation (including deep-links) and flows through the app

Server-driven UI from scratch illustration 1

The diagram above demonstrates a traditional mobile-server approach o top, and our new approach on the bottom - which adds a Backend-for-Frontend into the mix.

In the traditional approach (top), the mobile client app contains the definition of a screen - how it looks, what data it needs to fetch from the server, and how to plug that data into its UI.

By contrast, with a server-driven UI approach (bottom), the client only knows the name of a screen it needs to display (and potentially some additional arguments). The definition of a screen is held in the Backend-for-Frontend; when a client requests a certain screen, it is the BFF that makes any required requests to our server, plugs the resulting data into the screen structure, and returns a representation of the screen to the client for rendering.

A Backend-for-Frontend is a backend server application that services a specific client, typically focusing on one user experience. This design pattern was introduced originally at SoundCloud and popularized by Sam Newman.

Empower uses a variation on this pattern whereby a backend servers are positioned as a sort of middleware that sit between our client app and general API server. Their purpose is to prepare data from the server in ways specific for each client.

This approach is powerful. It prevents domain logic from living in the client app, and decouples the apps deployment lifecycle from deployment lifecycle of the actual business code. It means the client doesn’t even know that what kind of app it is! By updating and re-deploying the BFF, you could change the entire app in an instant.

Technology decisions

The following are a few of the more impactful tech choices we made when designing this system:

  • Language:** Kotlin **using the Ktor** **framework
    • We’re using Ktor as both a server (receiving GraphQL requests from the mobile app) and a client (making requests to our server)
    • We wanted to use Kotlin as its familiar to many of our client developers, and felt its important that the BFF (and the screens and features it defines) are managed by front-end engineers.
  • Communication Protocol: GraphQL
    • This takes care of the transport layer, allows us to generate client side models from the GraphQL schema, and takes care of some versioning concerns (clients will only request elements and fields that they can support)
  • Deployment, Infrastructure: The app is Docker**ized **and deployed to an **Azure App Service, **which is managed in terraform.
  • Monitoring: The app emits logs and telemetry consumed by Azure App Insights.

Building blocks

We’ve built our server-driven UI system to meet the requirements and capabilities we need in mobile apps. A lot of the concepts are similar to those found in declarative UI frameworks like Jetpack Compose, since that is what we’re effectively doing - serving a declarative UI remotely.

Components

Components make up the visual structure of our server-driven UI system.

Components can be a single atomic UI element, or a structure made up of multiple elements, or a container for other components.

Here are some of the examples our BFF can serve to clients :

Button component

Loan Summary component

Instruction List component

Some components are static, while others can read or write state - such as text fields or checkboxes.

We current maintain about about 40 components, each of which increases the scope of features we can build without a client release.

Actions

Pretty self descriptive, Actions describe what the client should do when various components are interacted with. Common action we use include:

  • Navigate to a new screen
  • Submit a form
  • Show a modal
  • Start a 3rd party SDK

Many actions also include the following:

  • An analytics event to fire when the action is invoked
  • A follow up Action to fire when the original Action has completed. For example, an Action to start a 3rd party integration like Veriff KYC would have a follow up action to reload the screen post-ID verification

Screens

A screen definition is defined in Kotlin code; when a screen is requested by a client, the BFF makes API calls to our server (if necessary for that screen) then builds up a Screen Response containing a **User Interface **and an initial Screen State. These are then returned to the client and displayed by a rendering layer in the mobile app; this maps Components to equivalents in Jetpack Compose (Android) or Swift UI (iOS).

User interface

The User Interface contains the Components to be displayed on screen, divided up based on which section of the screen they live in:

  • Top bar (typically a toolbar)
  • Content (main scrolling content of the screen)
  • Bottom bar (pinned to bottom of screen, typically CTA buttons and important disclaimers)

It also contains metadata about the screen itself like:

  • System styling (status and navigation bar color)
  • Lifecycle Actions eg. on back press, on load

You can see an example of a User Interface response below.

Example

Here is an example screen definition from our KYC flow. It contains:

  • A header with title and subtitle
  • An info list with 2 cells
  • A pinned bottom bar, containing a disclaimer label + a button with a NavigateScreenAction to the next screen in the flow.

{
	"top": {
		"type": "Toolbar",
		"title": null,
		"navButton": "Back"
	},
	"content": [
		{
			"type": "Header",
			"title": "Add User Profile",
			"subtitle": "Provide the following information in a few steps, to become verified and get a loan."
		},
		{
			"type": "InfoList",
			"items": [
				{
					"title": "Personal information",
					"subtitle": "Estimated completion time: 2 minutes",
					"icon": "Statement",
					"color": "PastelPink"
				},
				{
					"title": "Work information",
					"subtitle": "Estimated completion time: 2 minutes",
					"icon": "Wheel",
					"color": "PastelYellow"
				},
			],
			"backgroundColor": "LightGrey"
		}
	],
	"bottom": [
		{
			"type": "Text",
			"text": "Please make sure to provide correct and accurate information. Once submitted, changes will no longer be allowed.",
			"textStyle": "Small/DarkGrey"
		},
		{
			"type": "Button",
			"label": "Get started!",
			"action": {
				"actionType": "NavigateScreen",
				"screenKey": "KYCPersonalInfo"
			}
		}
	]

The client logic receives this payload from the BFF, and maps each component into Jetpack Compose components to render a screen that looks like this:

As you can see, with the generic mapping logic in place you could easily change the apps behavior by altering the screen data returned by the BFF:

  • You could update copy, add an additional InfoList item, or add another component to the screen
  • You could change the navigation flow by changing the screen navigated to in the NavigateScreenAction

Server-driven UI from scratch illustration 5

Outcomes

We are already seeing the benefits of this approach realized:

  • Our recent release cadence has been 2-3 BFF releases for every 1 client app release. We expect this to improve further as additional parts of the app are converted to SDUI, and our component and action libraries grow to cover more use cases.
  • We’ve recently done our first product experiment. We were able to push a flow change and new screen to production in <2 hours using a BFF release. This would have taken 1-2 weeks using a traditional approach with a client release
  • We were able to release a new feature and major navigation change to our Loan Application flow by simply deploying an updated version of our BFF, while our Android client release was blocked by a slow Google Play review.

Future

At the moment, our new Cashalo app is still at small scale. Our first priority going forward is expanding the capabilities and reliability of our SDUI system as we scale. Once we’ve cleared those hurdles, a couple of other items on our roadmap are:

  • Potentially expand use of this system across other teams in Empower, or inspire/support similar efforts (this is already happening!)
  • Currently, all our screens are defined in Kotlin code on the BFF. Eventually, we’d like to move them out of our codebase into a database somewhere.
  • That opens up the option of creating a web-based internal tool, for our product team to build screens and flows without involving an engineer, and save them to the database to be served by the BFF

About Hamish Cundy

Hamish Cundy

👋 I’m Hamish, a mobile engineer from New Zealand who enjoys designing and implementing innovative software solutions. I’ve been working in the mobile space for about a decade, building apps in the utility, parking, banking and finance sectors. I aspire to work on and build solutions that make a positive impact on the world and peoples lives.

I split my time between Melbourne, Australia, and the beautiful South Wairarapa coast of New Zealand.

In my spare time I enjoy photography (mostly via drone), cycling and hiking. I also enjoy the frequent travel involved in my current role; getting to meet up with my colleagues in a new and exciting place twice a year makes all the difference when working remotely day-to-day.

Connect with Hamish

Keep reading

More in Engineering