In a world where app updates can be a lengthy process, Duolingo's new usage of server-driven UI (SDUI) is a game-changer. By handling UI from the backend, we've sped up development and opened doors for quick experiments and consistent user experiences. Keep reading to find out how we did it. And if you want to work at a place that takes engineering innovation seriously, we’re hiring.
Background and motivation
At first glance, this screenshot of the shop may look like a typical Duolingo screen. However rather than being rendered by standard client code, it’s delivered via server driven UI from the backend, which allows us to streamline the development process.
Testing assumptions is a key operating principle at Duolingo. Consider our shop interface: we hypothesized that transforming the inventory section into a scrollable carousel would enhance user interaction by offering a cleaner UX with more spacious items and flexibility for introducing new items.
Traditionally, UI updates involve a lengthy process: a developer implements changes, the app is submitted to the App/Play Store, and upon approval, the new version is released. Even minor updates can take a week minimum for users to see. If a bug, like incorrect item width in the carousel inventory example, is found post-release, the fix requires another full week. Dealing with the release cycle is two-fold because of separate implementations on each client.
This standard process has pain points: the release cycle slows experimentation, users on different versions have varied experiences, and changes require separate implementation on each platform. SDUI addresses these by sending UI instructions directly from the backend to the client. No more waiting for release cycles. Welcome, rapid experimentation and bug fixes for a smoother user experience. All app versions receive the same UI, and a single backend change can apply to multiple clients, reducing development time.
While many companies have built SDUI systems, Duolingo's is uniquely flexible to support the app’s creative designs. The system's granularity allows control over everything—from UI layout to interactivity—via a single backend response.
SDUI at Duolingo
To integrate SDUI in the app, we use a SDUIResponse, which details information about screens, UI components, styles, and a data model.
Building UI
Components
Components are fundamental UI building blocks, akin to UIView in iOS, View in Android, or HTML Element on the web. Below is a breakdown of components needed to build out the carousel from the inventory example, including labels, images, stacks, buttons, and carousels.
The SDUI API emphasizes composability, allowing developers to break down UI into manageable pieces. We currently have 12 components in the API, with the flexibility to add more.
Stylesheet
Components have customizable appearance properties like borders and background colors, stored in a shared stylesheet. More appearance properties can be added as needed.
The API is essentially a UI library with versioning. When adding to the API, there are backend changes so that developers can use the new elements, and client changes so that the frontend can parse and display the new elements.
Screens
A screen is a collection of UI elements presented as a single page. The screen part of the response lists components for each screen.
Achieving interactivity
With screens, components, stylesheet defined, we can now build how a UI looks, but something else is missing. What about how users can interact with the UI?
Actions
Take the following example: the shop is inherently a set of buttons. Suppose you run out of hearts and you go to the shop and scroll to the hearts section and you see these two buttons. When a button is tapped, what can happen?
Users might expect, tapping on the heart refill item might trigger buying the hearts, or navigating to a new screen. These are examples of actions: executables triggered by user interactions. Actions can occur when components are tapped, shown or when screens are loaded, shown or dismissed. They are responsible for adding interactivity within SDUI screens.
Conditions
Continuing with the hearts refill example, if the user has the max number of hearts, we shouldn’t offer a refill option. To achieve this, we can have a show condition that user hearts should be less than the max number of hearts to show the heart refill button component. Conditions determine component visibility and viability of action execution based on user state.
Saving user state
Now we know how to build UI and how interactions in the UI work, we will talk about the last portion of the response: the data model.
We can consider the UI config to be the three pieces we have talked about so far: screens components and stylesheet. The UI configuration contains definitions of which component should look like under what circumstances, and the data model layer is a store of values that can be changed, specific to each user. The UI configuration acts as a view that hooks into the data model as the source of truth. It contains essential data such as asset images, text data (strings for labels), and backend values used for evaluating conditions.
There are three main reasons to separate the response into two layers:
- Support localization
- Allow dynamic local data updates
- Support versioning
Supporting localization
The data model enables efficient localization by sending only the user’s current UI string translation, avoiding the bloat of returning all possible string localizations.
Dynamic local updates
By caching every data model response locally, we can update what is displayed on the client dynamically without requiring a new backend response, allowing various local update actions to change data values as needed.
Supporting versioning
Say we want to add a new piece to the API, such as a new component. In order to do this, we will have to make a client change to reflect the new component. But what happens to older clients that can’t display the new component?
This is where the separation of UI configs and data model layer becomes useful. The goal is to make sure that all the server driven screens can still show up on outdated clients even if they can’t parse the latest UI configurations sent by backend. We call this versioning.
Our approach to versioning is to make the client cache the UI configuration. This cached UI configuration is guaranteed to be displayable by the client. If a client version is too old to parse the most up-to-date version of the SDUI response, the server knows to only send the data model and skip sending the UI config. This is called a partial response.
When an old client receives a partial response, it will still be able to render a screen, but it will use the locally cached UI config that does not have the new component. The data fields such as text, asset, and item prices will be up to date because we have the freshest version of the data model.
This approach to versioning makes it much easier to maintain the SDUI system over a long period of time, because developers don’t need to add tons of manual client version checks on the backend for every client change, which would make the codebase messy and difficult to maintain.
Conclusion
Since developing the MVP system a year ago, we have:
- run 18 different experiments using SDUI, including another full redesign of the shop that required no client changes
- fixed countless bugs, some of which would have ordinarily been considered release blockers
- expanded the scope beyond the shop to include the purchase flow
SDUI at Duolingo boosts development speed and ensures a uniform user experience. It's flexible for different UI designs, allows experiment rollouts independent of client releases, and will support cross-platform development in the future.
To wrap up, here are some key takeaways from building the SDUI API:
- Design for scalability from the start. Make sure to consider how to make the API flexible for future additions.
- Consider how versioning works. Long term, versioning is a part of scalability. It’s important to take a realistic view of app lifecycles.
- Scalable systems afford tighter scopes. Good, scalable API design helps to set clear boundaries for what is in the initial API and limits the timelines for the first iteration.
Looking for a workplace that prioritizes engineering efficiency and innovation? We're hiring!