Backend-Driven UI or how to speedup feature delivery without frequent releases

published: ⏰ 15

I recalled an interesting user interface-related task I had to tackle a long time ago. There were situations where changes needed to be delivered quickly to users without going through a full release cycle with application refinements. The article below discusses how this can be implemented and accelerated for mobile, desktop, and web applications.

Introduction

Let me give an example from the past, for transportation and driver apps. Both critical and minor errors are often discovered in the app during operation. For example, necessary fields are missing on the order information screen, without which it cannot be confirmed and fulfilled. In a typical situation, this requires:

  1. Receiving information about the incident and assigning a fix.
  2. Fixing the problem by the developer.
  3. Building and deploying the new release to the test rig.
  4. Initiating the testing process.
  5. Publishing the new version in the marketplace and awaiting approval (if it’s a publicly available app).
  6. Distributing the update to drivers’ devices.

All of this can take a significant amount of time, which can lead to downtime if there are no alternative ways to obtain the necessary information.

One approach to solving this problem is to implement a Backend-Driven UI (BDUI) or Server-Driven UI (SDUI). While in the classic approach, the server is solely responsible for the data, and the client for its display, in the BDUI model, the server takes responsibility for the interface structure, the layout of elements, and even some of the interaction logic.

A decade or more ago, when developing applications (Android/iOS), there was an idea to implement a BDUI. But if I remember correctly, it ended at the Proof-of-Concept stage, and I never completed it. For web applications, however, I often had to create administrator-configurable forms for various parts of the system. This was often implemented through a JSON description of the form structure, which was then displayed on the client.

Interface Development Approaches

One of the oldest and most well-known approaches is Server-Side Rendering (SSR). I’m sure many people have worked with websites written in PHP, JSP, Perl, or CGI scripts, where the interface was rendered entirely on the server. The server generated ready-made HTML code, which was sent to the browser. The browser acted as a simple viewer for the generated content. All display logic was tightly bound to the server code. This ensured consistency—what the user saw was what was deployed on the server. This had its drawbacks: increased server load (rendering) and low interactivity (all actions were handled through server requests).

Then, with the development of JavaScript, client-side web applications began to appear – Client-Side Rendering (CSR). Frameworks like Angular, React, and Vue allowed the display logic to be moved to the client. The server began returning only JSON data, and the browser assembled the interface from it. This made it possible to create interactive web applications—Single Page Applications (SPA), where transitions between screens occurred instantly, with only the necessary data loaded from the server.

In mobile development, an analog of the SPA approach existed from the very beginning for native iOS, Android, and Windows Mobile/Windows Phone apps (and there were some examples). Apps acted as “thick clients,” containing all the UI logic, animations, navigation, and business rules. The server only returned data via an API. This provided excellent performance, reducing the load on servers and allowing offline operation, but created the problem mentioned above—a tight coupling between the app version and the interface version.

One attempt to solve this problem in mobile development was the use of various WebViews—embedded browsers within native apps (combining CSR and a native mobile app). This allowed for instant interface updates in seemingly native apps. However, there were certain downsides: the interface looked and felt different from native elements, performance was lower, and access to native device features was limited or required the development of additional adapters.

Backend-Driven UI (BDUI) can be seen as an extension of server-side rendering (SSR), adapted for native apps. Unlike WebView, BDUI doesn’t generate ready-made HTML (it might not even be HTML). Instead, the server sends a declarative description of the interface (for example, in JSON), which the client app interprets and displays using native components.

This is a key difference. When using BDUI, the button the user sees is not a <button> web element, but a real UIButton on iOS or android.widget.Button on Android. This provides high performance and all the capabilities of native platform components, while maintaining the flexibility of the web approach. The server decides which components to display and in what order, while the client decides how these components appear and behave on a specific platform.

Thus, BDUI occupies a niche between native apps and web apps, combining the advantages of both approaches.

Concept and Architecture

The examples below are given in the context of mobile apps, but the same principles apply to web and desktop apps.

In a classic mobile app architecture, the data flow looks like this:

  1. The user opens a screen.
  2. The client sends a GET /api/profile request.
  3. The server returns data: { "name": "Evlampiy", "age": 150 }.
  4. The client displays the data in UI elements (pre-defined by the developer).

In a BDUI architecture, the data flow changes as follows:

  1. The user opens a screen (or the application requests a screen configuration).
  2. The client sends a GET /api/screen/profile request.
  3. The server returns a component tree:
    JSON
    {
      "type": "Container",
      "layout": "vertical",
      "children": [
        { "type": "Text", "content": "Profile", "style": "header" },
        { "type": "Text", "content": "Evlampy", "style": "body" },
        { "type": "Text", "content": "150 years", "style": "body" }
      ]
    }
  4. The client-side Renderer receives this JSON, recursively traverses the tree, and creates a corresponding native component for each node.

The example below shows a simplified diagram of the data flow in BDUI:

A client-side UI Schemas base is not required, but is needed if the application needs to work offline without access to the server (native, PWA).

ОImplementation Features

For implementing a BDUI, four essential parts of the system can be identified:

  1. Component library.
  2. Rendering engine.
  3. Interface schemas.
  4. Interface builder.

It’s also important to define where the business logic boundary lies:

  • If the server sends an email input form, should the client check the email format before sending? The client should perform basic validation for instant feedback (fail-fast), but the server should also perform final validation.
  • If shipping costs need to be calculated, this should happen on the server. The client can display the result, but should not calculate it to avoid desynchronization and manipulation.
  • The client must be aware of its screens. The server can say “go to the profile screen,” but the client must know which native controller corresponds to the profile_screen identifier.

Component Library

The component library is the foundation of the client-side component. The client application should contain a pre-implemented set of widgets that the server can use to render according to the schema. Typically, this set includes:

  • Basic elements – text, image, button, various input fields, checkboxes, and so on.
  • Containers – vertical/horizontal list, grid, form, and so on.
  • Complex components – map, video/audio player, calendar, charts.

It’s important to understand that this library must be parameterizable. For example, the server must be able to change the component display styles. Instead of a command like “show a red button,” the server should send { "type": "Button", "style": "danger" }, where the danger style is displayed in red on the client. This allows the client to change the presentation without changing the server contract.

Rendering Engine

The rendering engine is the core of the client-side. Its job is to parse JSON/XML schema and convert it into a graph of interface objects. The renderer must be recursive, as the UI structure is a tree. It must be able to:

  • Create instances of component classes using properties passed from the server (text, colors, margins) and taking into account the constraints of the containers in which these components are placed (layouts).
  • Bind data from the server to interface elements. This is a two-way communication – we display and receive user input. For example, if the server sends {"name": "John", "age": 25}, we need to display this in the corresponding fields and send back to the server the user’s input.
  • Be able to handle user action events: button clicks, scrolling, gestures, data entry.
  • Validate user input.
  • Correctly handle interface layout rendering errors. For example, if a component description is incorrect or unsupported, display placeholders without completely crashing the application.

Interface Schemas

Interface schemas (UI Schema / DSL) are essentially the language of communication between the server and the client. JSON is most often used due to its apparent readability and widespread support. However, in my opinion, JSON is poorly suited for this purpose due to its inherent limitations. XML is still more compact and predictable for describing typed interfaces. Here, it is important to spend more time designing the schema, taking into account all the necessary requirements for the rendering engine.

The schema should support:

  • A hierarchy of components, since the interface description is almost always a tree, not a flat list.
  • Setting conditions for component rendering and validation. Often implemented through client-side logic based on context data.
  • Description of actions during user interaction with the interface.
  • The ability to bind components to data (e.g., { "type": "Text", "value": "@user.name" }).

To describe client logic, the diagram can use interpreted languages ​​(for example, JavaScript - unsafe in some cases) or a custom DSL.

Interface Builder

UI Builder – While it’s possible to manually create schemas (JSON/XML) and store them in code, in practice a tool for visually creating and managing screens is essential. This could be an internal admin panel where managers or developers create screens from pre-built blocks, previewing them, and then publishing them. Without such a tool, the advantage of BDUI (speed of change) is lost, as any schema change in the backend code will still require a server-side deployment.

It’s important to note that not all changes can be implemented without a new server-side release. For example, a change to the data model will always require a new version (unless the model is dynamically generated).

How to implement this

I decided not to create a sample project for implementing BDUI, as it’s highly platform- and technology-dependent (Web/JS, iOS/Swift, Android/Kotlin). Each platform implements it differently, even at the schema level. Therefore, I don’t think it would be particularly useful, and it’s not a quick task.

Schematic

The interface description schema might look like this:

JSON
{
  "screen_id": "login_v1",
  "version": "1.0.0",
  "components": [
    {
      "id": "header_logo",
      "type": "Image",
      "properties": {
        "url": "https://cdn.example.com/logo.png",
        "contentMode": "fit",
        "accessibilityLabel": "Company logo"
      },
      "style": {
        "marginTop": 40,
        "marginBottom": 20,
        "alignment": "center"
      }
    },
    {
      "id": "email_input",
      "type": "TextField",
      "properties": {
        "placeholder": "Enter your email",
        "keyboardType": "emailAddress",
        "validationRule": "email"
      },
      "style": {
        "marginHorizontal": 16
      }
    },
    {
      "id": "password_input",
      "type": "TextField",
      "properties": {
        "placeholder": "Password",
        "isSecure": true
      },
      "style": {
        "marginHorizontal": 16,
        "marginTop": 10
      }
    },
    {
      "id": "login_button",
      "type": "Button",
      "properties": {
        "text": "Login",
        "action": {
          "type": "submit",
          "endpoint": "/api/auth/login",
          "method": "POST",
          "payload": {
            "email": "@email_input.value",
            "password": "@password_input.value"
          }
        }
      },
      "style": {
        "marginHorizontal": 16,
        "marginTop": 20,
        "theme": "primary"
      }
    }
  ]
}

Here, the actions on the login_button button are described using the action field. This action declaratively defines, without scripting, what should happen when the button is clicked. The client must be able to interpret this action, collect data from the specified fields (@email_input.value, @password_input.value), and execute the request.

The same approach is used to generate a platform-specific interface (iOS, Android) on the client, with data binding.

Client-Side State Management

One important aspect is managing state or data context. When a user enters text into a field, this state should be accessible to other components (for example, a submit button, which may be inactive while the field is empty). Synchronization between components can be organized in two ways:

  1. Through local state, where each component stores its own value. Events (Event Bus) are used for communication. For example, an input field sends a TextChanged event, and the button subscribes and checks for validity.
  2. Through global state in the form of a single data tree corresponding to the UI tree. The renderer maintains a reference to this tree. When the input field changes, it updates the value in the data tree. When displayed, the button reads the value from the data tree.

I’ve most often implemented the second approach. It’s more predictable and easier to debug, since the interface is a pure function of state.

Security and Validation

Using BDUI significantly expands the attack surface of the application. Since the server controls the interface, an attacker can attempt to manipulate the server or client to change the application’s behavior.

The basic rule is: never trust the client. Just because the server sent a “Transfer 0 rubles” button doesn’t mean the client must allow the transfer. It’s important to consider:

  • All calculations, access rights checks, and data validation must be duplicated on the server. Client-side validation is for user experience (UX) purposes only.
  • If the server sends an action to open a link, the client must check the link domain against a whitelist. Allowing arbitrary websites to open is not permitted unless specifically required.
  • If the server allows inserting custom text into components (e.g., comments), it’s necessary to ensure that this text is displayed securely on the client (HTML escaping, disabling script execution if WebView is used within components).

To prevent schema manipulation, the following measures can be taken:

  • Always use encrypted traffic (HTTPS).
  • Mandatory verification of authorized certificates (SSL Pinning).
  • In critical scenarios (e.g., finance), the server can digitally sign the schema. The client verifies the signature before displaying it. This ensures that the schema has not been modified in transit.

Performance and Optimization

I think this point is debatable, given the power of modern devices, but it’s worth mentioning.

Parsing large schemas can take time on the main thread (UI rendering is almost always single-threaded), causing UI lag. A solution could be to use asynchronous schema parsing and UI component generation (if the platform allows). Additionally, binary schema description formats (e.g., Protobuf) could be used.

On the UI side, specific optimizations can be applied, such as caching components or entire screens to avoid recreating them for each schema display.

On the network side, there may be delays because the interface is loaded from the server; the user may see a blank screen for longer than usual. The following approaches can help:

  • Cache screen schemas on the client (in the diagram above, this is the UI Schemas database). Schema updates occur in the background. * If the user navigates through the checkout process (Catalog -> Product -> Cart), you can preload the Cart screen layout while the user is viewing the Product screen.
  • Show a skeleton placeholder to let the user know that content is coming soon. This gives the impression that the app is working and not frozen.

However, this isn’t all that critical if the layouts aren’t too large.

Versioning and Compatibility

This is perhaps the most complex organizational and technical issue in BDUI. You will always have a fleet of devices with different app versions. An older app version (1.0) may not be aware of the new VideoPlayer component introduced in version 2.0.

As mentioned above, the rendering engine must correctly handle situations with unknown components:

  • If the renderer encounters an unknown type, it simply doesn’t display it or displays a blank space. This is safe, but it can break the layout (for example, the submit button disappears).
  • The renderer displays a standard block with the text “Update your app to view this content.”
  • The server knows the client version (for example, it’s passed in the headers). If the client is old, the server can send a simplified version of the layout (without video, just an image). This requires supporting multiple versions of logic on the backend.

The last point requires supporting multiple schema versions on the backend and in the interface builder, which can significantly complicate the architecture. However, it does allow support for legacy clients.

A simpler approach, which has been used primarily in the past, is forced updates. If the changes are critical and cannot be backward-compatible (for example, if a model has changed), the server must have a forced update mechanism. A mandatory update flag can be returned in the response. The client simply won’t be able to continue working without an update. This simplifies the architecture, but can cause user frustration (users often don’t update until the last minute).

Risks and Issues

I might be missing something, but I think it can be summarized in the following table:

RiskDescription
Security- The client can’t be trusted. All business logic and validation must be on the server.
- Risk of injection through dynamic content.
Performance- Overhead for parsing schemas and building the interface.
- Increased traffic volume (schema text weighs more than just the data).
Offline mode- Cached data and schema can’t always be displayed.
- Require a connection to the server after a certain period of time.
Testing complexity- Difficult to test an infinite number of UI combinations.
- Client-side schema caching further exacerbates the situation.
UI limitations- The set of available components is limited.
- Constant refinement is required (in the early stages).
Implementation complexityCreating a reliable and fully functional framework is not a trivial task and may require significant capital expenditures as well as operational support costs.

Tools and Ecosystem

To be honest, I’ve never used ready-made solutions. I’ve always used custom ones.

Open-source solutions I’ve seen:

  • DivKit is an open-source framework from Yandex for implementing Server-Driven UI (SDUI).
  • Duit is a rapidly developing ecosystem of open-source libraries and tools designed to help developers easily and effectively implement a backend-driven UI approach in their Flutter apps.
  • Zup Beagle – appears to be abandoned.
  • React Server Components (RSC) – allow you to generate components on the server and transfer them to the client. This is conceptually close to BDUI, but in the React ecosystem.
  • HTMX – allows you to receive HTML fragments from the server and insert them into the DOM. You can see an application example in the article Image Recognition Web Service in Go with HTML.

Large companies often write their own solutions. This is because ready-made frameworks can be redundant, don’t cover specific business cases, or simply don’t fit into their internal technical stack.

Is it worth implementing a BDUI in your project?

Before making a decision, it’s important to understand whether a BDUI is truly feasible. Typically, people start by implementing applications in a classic style, and the understanding of whether it’s needed or not comes along the way.

Yes, if:

  • Frequent changes to the data representation content are required.
  • Marketplace release time is critical – T2M (less than 24 hours for changes).
  • Resources are available to support a separate team or dedicated engineers for framework development (if an internal framework is being developed).
  • It is necessary to guarantee a consistent interface appearance across different platforms (not visually, but in terms of content).

No, if:

  • The application is stable and changes every six months.
  • A small team (2-3 developers) who cannot support a complex infrastructure.
  • The application requires complex graphics, animations, or hardware (AR, video processing), where native code is critical. * There is no clear understanding of the application’s goals and design, components are constantly changing chaotically.

Conclusion

To put it all together, this approach was arrived at through an evolutionary process. It emerged that conventional development and delivery methods were unsuitable or too time-consuming to implement. However, this flexibility comes at a price. The challenge is interesting, but it requires significant engineering effort, process changes, and consideration of security and performance issues.

Ui Mobile Bdui Sdui