Background: Server-Driven UI
Before we dive into Airbnb’s implementation of server-driven UI (SDUI), it’s important to understand the general idea of SDUI and how it provides an advantage over traditional client-driven UI.
In a traditional world, data is driven by the backend and the UI is driven by each client (web, iOS, and Android). As an example, let’s take Airbnb’s listing page. To show our users a listing, we might request listing data from the backend. Upon receiving this listing data, the client transforms that data into UI.
This comes with a few issues. First, there’s listing-specific logic built on each client to transform and render the listing data. This logic becomes complicated quickly and is inflexible if we make changes to how listings are displayed down the road.
Second, each client has to maintain parity with each other. As mentioned, the logic for this screen gets complicated quickly and each client has their own intricacies and specific implementations for handling state, displaying UI, etc. It’s easy for clients to quickly diverge from one another.
Finally, mobile has a versioning problem. Each time we need to add new features to our listing page, we need to release a new version of our mobile apps for users to get the latest experience. Until users update, we have few ways to determine if users are using or responding well to these new features.
The Case for SDUI
What if clients didn’t need to know they were even displaying a listing? What if we could pass the UI directly to the client and skip the idea of listing data entirely? That’s essentially what SDUI does — we pass both the UI and the data together, and the client displays it agnostic of the data it contains.
Airbnb’s specific SDUI implementation enables our backend to control the data and how that data is displayed across all clients at the same time. Everything from the screen’s layout, how sections are arranged in that layout, the data displayed in each section, and even the actions taken when users interact with sections is controlled by a single backend response across our web, iOS, and Android apps.
SDUI at Airbnb: The Ghost Platform
The Ghost Platform (GP) is a unified, opinionated, server-driven UI system that enables us to iterate rapidly and launch features safely across web, iOS, and Android. It is called Ghost because our primary focus is around ‘Guest’ and ‘Host’ features, the two sides to our Airbnb apps.
GP provides web, iOS, and Android frameworks in each client’s native languages (Typescript, Swift, and Kotlin, respectively) that enable developers to create server-driven features with minimal setup.
The core feature of GP is that features can share a library of generic sections, layouts, and actions, many backward compatible, enabling teams to ship faster and move complicated business logic to a central location on the backend.
A Standardized Schema
The backbone of the Ghost Platform is a standardized data model that clients can use to render UI. To make this possible, GP leverages a shared data layer across backend services using a unified data-service mesh, called Viaduct.
The key decision that helped us make our server-driven UI system scalable was to use a single, shared GraphQL schema for Web, iOS, and Android apps — i.e., we’re using the same schema for handling responses and generating strongly typed data models across all of our platforms.
We’ve taken time to generalize the shared aspects of different features and to account for each page’s idiosyncrasies in a consistent, thoughtful way. The result is a universal schema that’s capable of rendering all features on Airbnb. This schema is powerful enough to account for reusable sections, dynamic layouts, subpages, actions, and more, and the corresponding GP frameworks in our client applications leverage this universal schema to standardize UI rendering.
The GP Response
The first fundamental aspect of GP is the structure of the overall response. There are two main concepts used to describe UI in a GP response: sections and screens.
- Sections: Sections are the most primitive building block of GP. A section describes the data of a cohesive group of UI components, containing the exact data to be displayed — already translated, localized, and formatted. Each client takes the section data and transforms it directly into UI.
- Screens: Any GP response can have an arbitrary number of screens. Each screen describes the layout of the screen and, in turn, where sections from the
sections
array will appear (called placements). It also defines other metadata, such as how to render sections — e.g., as a popover, modal, or full-screen — and logging data.
A feature’s backend built with GP will implement this GPResponse
(fig. 2) and populate the screens and sections depending on their use case. GP client frameworks on web, iOS, and Android provide developers standard handling to fetch a GPResponse
implementation and translate that to UI with minimal work on their part.
Sections
Sections are the most basic building block of GP. The key feature of GP sections is that they are entirely independent of other sections and the screen on which they are displayed.
By decoupling sections from the context around them, we gain the ability to reuse and repurpose sections without worrying about a tight coupling of business logic to any specific feature.
Section Schema
In GraphQL schema, GP sections are a union of all possible section types. Each section type specifies the fields they provide to be rendered. Sections are received in a GPResponse
implementation with some metadata and are provided through a SectionContainer
wrapper, which contains details about the section’s status, logging data, and the actual section data model.
One important concept to touch on is SectionComponentType
. SectionComponentType
controls how a section’s data model is rendered. This enables one data model to be rendered in many different ways if needed.
For example, the two SectionComponentType
s TITLE
and PLUS_TITLE
might use the same backing TitleSection
data model, but the PLUS_TITLE
implementation will use Airbnb’s Plus-specific logo and title style to render the TitleSection
. This provides flexibility to features using GP, while still promoting schema and data reusability.
Section Components
Section data is transformed into UI through “Section Components”. Each section component is responsible for transforming a data model and a SectionComponentType
into UI components. Abstract section components are provided by GP on each platform in their native languages (i.e., Typescript, Swift, Kotlin) and can be extended by developers to create new sections.
Section components map a section data model to one unique rendering and therefore pertain only to one SectionComponentType
. As mentioned previously, sections are rendered without any context from the screen they are on or the sections around them, so each section component has no feature-specific business logic provided to it.
I’m an Android developer, so let’s take an Android example (and because Kotlin is great ). To build a title section, we have the code snippet seen below (fig. 5). Web and iOS have similar implementations — in Typescript and Swift respectively — for building section components.
GP provides many “core” section components, such as our example TitleSectionComponent
above (fig. 5), meant to be configurable, styleable, and backward compatible from the backend so we can adapt to any feature’s use case. However, developers building new features on GP can add new section components as needed.
Screens
Screens are another building block of GP, but unlike sections, screens are mostly handled by GP client frameworks and are more opinionated in usage. GP screens are responsible for the layout and organization of sections.
Screens schema
Screens are received as a ScreenContainer
type. Screens can be launched in a modal (popup), in a bottom sheet, or as a full screen, depending on values included in the screenProperties
field.
Screens enable dynamic configuration of a screen’s layout and, in turn, arrangement of sections through a LayoutsPerFormFactor
type. LayoutsPerFormFactor
specifies the layout for compact and wide breakpoints using an interface called ILayout
, which will be elaborated on below. The GP framework on each client then uses screen density, rotation, and other factors to determine which ILayout
fromLayoutsPerFormFactor
to render.
ILayouts
ILayout
s enable screens to change layouts depending on the response. In schema, ILayout
is an interface with each ILayout
implementation specifying various placements. Placements contain one or many SectionDetail
types that point to sections in the response’s outermost sections
array. We point to section data models rather than including them inline. This shrinks response sizes by reusing sections across layout configurations (LayoutsPerFormFactor
from fig. 7).
GP client frameworks inflate ILayout
s for developers, as ILayout
types are more opinionated than sections. EachILayout
has a unique renderer in each client’s GP framework. The layout renderer takes each SectionDetail
from each placement, finds the proper section component to render that section, builds the section’s UI using that section component, and finally, places the built UI into the layout.
Actions
The last concept of GP is our action and event handling infrastructure. One of the most game-changing aspects of GP is that in addition to defining the sections and layout of a screen from the network response, we also can define actions taken when users interact with UI on the screen, such as tapping a button or swiping a card. We do this through an IAction
interface in our schema.
Recall from earlier (fig. 6) that a section component is what translates our TitleSection
to UI on each client. Let’s take a look at the same Android example of a TitleSectionComponent
with a dynamic IAction
fired on the click of the subtitle text.
When a user taps the subtitle in this section, it fires the IAction
passed for the onSubtitleClickAction
field in TitleSection
. GP is responsible for routing this action to an event handler defined for the feature, which will handle the IAction
that was fired.
There is a standard set of generic actions that GP handles universally, such as navigating to a screen or scrolling to a section. Features can add their own IAction
types and use those to handle their feature’s unique actions. Since the feature-specific event handlers are scoped to the feature, they can contain as much feature-specific business logic as they wish, enabling freedom to use custom actions and business logic when specific use cases arise.
Bringing It All Together
We’ve gone over several concepts, so let’s take an entire GP response and see how it’s rendered to tie everything together.
Creating the Section Components
Features using GP will need to fetch a response implementing GPResponse
mentioned above (fig. 2). Upon receiving a GPResponse
, GP infra handles parsing this response and building the sections for the developer.
Recall that each section in our sections
array has a SectionComponentType
and a section
data model. Developers working on GP add section components, using SectionComponentType
as the key for how to render the section data model.
GP finds each section component and passes it the corresponding data model. Each section component creates UI components for the section, which GP will insert into the proper placement in the layout below.
Handling Actions
Now that each section component’s UI elements are set up, we need to handle users interacting with the sections. For example, if they tap a button, we need to handle the action taken on click.
Recall earlier that GP handles routing events to their proper handler. The example response above (fig. 12) contains two sections that can fire actions, toolbar_section
and book_bar_footer
. The section component for building both of these sections simply needs to take the IAction
and specify when to fire it, which in both cases will be when a button is clicked.
We can do this through click handlers on each client, which will use GP infra to route the event on a click event.
button(
onClickListener = {
GPActionHandler.handleIAction(section.button.onClickAction)
}
)
Setting Up the Screen and Layout
To arrange a fully interactive screen for our users, GP looks through the screens array to find a screen with the “ROOT”
id (GP’s default screen id). GP will then find the proper ILayout
type depending on the breakpoint and other factors relevant to the specific device the user is using. To keep things simple, we’ll use the layout from the compact
field, a SingleColumnLayout
.
GP will then find a layout renderer for SingleColumnLayout
, where it’ll inflate a layout with a top container (the nav
placement), a scrollable list (the main
placement), and a floating footer (the footer
placement).
This layout renderer will take the models for the placements, which contain SectionDetail
objects. These SectionDetail
s contain some styling information as well as the sectionId
of the section to inflate. GP will iterate through these SectionDetail
objects and inflate sections into their respective placements using the section components we built earlier.
What’s Next for GP?
GP has only existed for about a year, but a majority of Airbnb’s most used features (e.g., search, listing pages, checkout) are built on GP. Despite the critical mass of usage, GP is still in its infancy and there’s much more to be done.
We have plans for a more composable UI through “nested sections”, improving discoverability of elements that already exist through our design tools, such as Figma, and WYSIWYG editing of sections and placements, enabling no-code feature changes.
If you’re passionate about server-driven UI or building UI systems that scale, there’s so much more to be done. We encourage you to apply to the open roles on our engineering team.
Re-engineering Travel Tech Talk
Server-driven UI is complex. Countless hours have gone into creating a robust schema, client frameworks, and developer documentation that enables GP to be successful.
If you’d like a more high-level overview of SDUI and GP, I recently had the opportunity to speak at Airbnb’s Re-engineering Travel tech talk presenting GP. I’d encourage you to check it out for a general overview of server-driven UI and GP (skip to the 31-minute mark if you’re short on time).
Special Thanks
Special thanks to Abhi Vohra, Wensheng Mao, Jean-Nicolas Vollmer, Pranay Airan, Stephen Herring, Jasper Liu, Kevin Weber, Rodolphe Courtier, Daniel Garcia-Carrillo, Fidel Sosa, Roshan Goli, Cal Stephens, Chen Wu, Nick Miller, Yanlin Chen, Susan Dang, and Amity Wang, as well as many more behind the scenes, for their tireless work building and supporting GP.