At QCon London 2024, Christopher Luu explained how Netflix uses server-driven UIs for rich notifications. This saves developer time through reuse across platforms and better testing but adds effort to maintain backward compatibility. Developers write notifications by embedding so-called Customer Lifecycle Component System (CLCS) components in JavaScript, similar to how React UIs embed HTML in JavaScript.
Notifications in Netflix apps tell users of important events, such as payment failures or promotions. Server-driven notifications allow Netflix to update the UI on the server without app updates, to share logic across the many platforms that Netflix supports, and to run A/B tests effectively. Developers can also create notifications on any platform, independent of their knowledge of platforms and programming languages.
The downsides are higher upfront costs for creating the notification framework and the need to keep the framework backwards compatible with older Netflix apps. It's also more challenging to support offline apps and debug notifications. Finally, some developers do not enjoy non-native development.
Netflix calls a notification UMA – Universal Messaging Alert. UMA runs on TV, web, iOS, and Android. Except for the web, all these platforms require Netflix to get approval for app updates from the TV vendor, Apple, or Google. That's where CLSC components, the server-driven UMA, save time & money: the same code runs on all platforms without app updates or approval. Still, Netflix only delivers UMA with these server-driven UIs: The apps use native, client-side UI frameworks for all other functionality.
UMA uses Hawkins, a visual design system named after the town from the Netflix show "Stranger Things". UMA uses an existing state machine and supports multistep interstitials with forward/backward navigation, such as updating payment information. The wire protocol is JSON.
Netflix did not want to reinvent HTML for UMA, so they built CLCS as a wrapper around Hawkins. CLCS abstracts backend logic away from the apps that render the UI with native UI controls and collect user input. It offers UI components like stacks, buttons, input fields, and images, as well as effects such as dismiss or submit.
Here is a CLCS example:
export function showBox(): Screen {
return (
<Modal>
<VerticalStack>
<Text typography='title' content='Come back next week' />
<Text typography='body' content='Please tune in next week to hear
Statler & Waldorf say: What do you call
an offline phone? - Notflix!' />
</VerticalStack>
</Modal>
);
}
The Netflix apps are responsible for the layout of notifications, such as landscape vs. portrait, positioning, and size. The apps still send information to the server in request headers, such as the app version and device information so that UMA may take advantage of this. Developers can create new CLCS components by combining other components with templates.
UMA still has to support old versions without CLCS. Why? Because Netflix has the challenge of supporting old app versions that will never be updated: mostly phones and tablets with OS versions that Netflix doesn't support anymore, but also TVs running older app versions. That's where GraphQL comes in: apps use GraphQL to request UMA from the server. This allows for the introduction of new features that older clients ignore, as they still ask for the old components. Additionally, new components specify a fallback method, which builds an alternate version with CLCS baseline components. Baseline components are guaranteed to be there.
Offline devices are not a major issue today, as offline devices cannot query the server for UMA. Still, Netflix may bundle some UMA as JSON in future app versions so that some notifications still show offline.
UMA uses demo-based tests for automated tests first. Here, a demo server delivers hard-coded UMA. Because it's clear how the Netflix app should render these, testers can take screenshots of the client app or run more specific integration tests. Netflix also runs backend template snapshot tests and traditional end-to-end tests.
Looking back, Luu listed a few things he wished he had finished earlier. These included the baseline components, a formalized testing strategy, better alignment with the design system partners, and alignment with the platforms on templates.