If you're a mobile app developer, perhaps you've dreamed of the flexibility of online development to make layout and logic changes on the fly and the ability to run hypothesis tests in seconds and process results even faster?

Mobile developers are taught to believe that the speed at which applications are released and updated is directly related to how quickly they get to users. App Store moderation time can be frustratingly long. Building a Software Development Kit (SDK) will be even slower because you have to fit your needs into someone else's product development and release cycles. It can be a good excuse not to test hypotheses at all.

In this blog post, we'll go over the topic and walk you through the steps of Backend-Driven Development, describing how it's used to solve specific problems and what benefits it has brought us. Materials for this post were taken from the source on the example of MovilePay. The author of the original text is Rodrigo Maximo.

What is Backend Driven Development?

Backend-driven development (or Backend-Driven Development, or Backend-Driven UI, or Server-Driven UI) is the concept of developing front-end applications based on server responses. The screens and flow change based on the server's responses.

The bulk of the work of creating mobile applications is usually associated with building a user interface: placing the elements with which the user interacts on screens so that he can quickly perform one or another action. Some of these components are filled with API payloads: typically JSON with primitive types (integers, booleans, strings) or application business processes.

The traditional approach to implementing screens has certain drawbacks, as there can be a lot of business logic on the application side, and the code becomes more difficult to maintain:

  • Need the same code for many platforms (Android, iOS, Web, etc.). This approach makes it harder to maintain cross-platform compatibility, which increases the chance of bugs and incompatibilities;
  • Each update or modification of a mobile application implies the need to change the code, which leads to an extended release of applications in the App Store;
  • In digital, A/B testing is more difficult. It is more challenging to test concepts and collect data from users to understand important product information;
  • Since most of the business logic is on the application side, the code is more challenging to maintain and maintain.

Backend-oriented development has arrived to solve these problems.

Consider the following scenario (which is based on the sample iOS app but can easily be translated to any other front-end project):

Scenario for backend driven development

{
    "pageTitle": "Demonstrative Title",
    "boxes": [
        {
            “type": “bigBlue",
            “title”: “Nice box”,
            “subtitle”: “Subtitle of box"
        },
        {
            “type": “smallRed",
            “title”: “Great box”
        },
        {
            “type": “rectangleGreen",
            “title”: “Incredible box”,
            “subtitle”: “Subtitle of box”,
            “number”: 10
        }
    ]
}

All business logic for displaying fields and displaying text and visual information is consolidated and abstracted on the server-side, eliminating the need for multiple interfaces to process this API. The server then applies the business logic and uses its results to generate an API response in JSON format.

In the example, to display blocks on the screen ("bigBlue", "smallRed" and "rectangleGreen"), additional information from each field is used. Interestingly, the “boxes” variable allows the backend to process as many blocks as the server gives.

Have you probably already guessed how you can conduct A / B testing in this situation? The server can select specific users to view only "bigBlue" blocks, while others can see all three types of blocks. Also, you can conduct A / B tests that change the order of displaying blocks on the screen.

Another use case for Server-driven Development is to make changes to the interface of an application. For example, if we need to change the headers in the application, it's enough to change the server's response simply. The display order of subtitles and blocks is also easy to change. In this case, you do not need to publish a new application release in the AppStore.

Making changes to the interface in backend driven development

{
    "pageTitle": "Demonstrative Title",
    "boxes": [
        {
            “type": “rectangleGreen",
            “title”: “Another title”,
            “subtitle”: “Another subtitle”,
            “number”: 100
        },
        {
            “type": “smallRed",
            “title”: “Different title”
        }
        {
            “type": “bigBlue",
            “title”: “Backend Driven”,
            “subtitle”: “Development”
        }
    ]
}

Think twice

There are a few things to keep in mind when using the Backend Driven Development approach. First, what degree of flexibility do you require?

Based on experience and research, we consider medium flexibility to be optimal.

You should not rush from one extreme to another. A large amount of freedom can negatively affect the payback of your development. You can't foresee everything, especially since device specs or screen size are out of your control. Finally, you end up with very confusing and overloaded application-side presentation logic. Also, you can't anticipate everything your design team might want to do in the future. Thus, you still have to make changes to the application code, and, during testing, numerous complex routes to achieve the goal will be discovered.

So, in conclusion, you won't need the flexibility that HTML has in the vast majority of cases. Therefore, before developing screens and server solutions that use the idea of ​​Backend-Driven Development, we suggest that you think through all possible options.

A Super App Creation, Movile Tech Case

You are probably familiar with the super app concept and have heard of an app like WeChat. WeChat is a mobile messaging platform and social network developed in China and has become extremely popular worldwide.

Such an application aims to collect several recurring services in one place and offer users a single point of access to most of the online queries from their daily lives. It is the holy grail of software development: putting together much functionality to make it all look like one, offering users simple answers to complex questions and solutions to big problems.

In the past, MovilePay has been involved in developing a super app, the process of developing an improved version of which is described in this article. It was supposed to be a testing and validation center where the team could test new services and find common ground in their use.

MovilePay had a problem creating an application that could quickly make changes to the code. It required testing many services and services and conducting research on user behavior based on the information displayed and testing hypotheses. MovilePay wanted to be able to turn features on and off quickly. Under such conditions, it was impossible to use the traditional approach with a release for every change. Considering all these points and a complex scenario, it was decided to apply Backend-Driven Development.

MovilePay created a category-based home screen to solve this problem. Each section has its own set of items called widgets. Sections displayed entry points for any already implemented services in any order and highlighted individual widgets.

Sometimes only one service was displayed. Other times there were three, depending on what was currently being tested. MovilePay left the choice to the server rather than hard-coding any rule into the application. As a result, the application only knows the definition of the service's entry point and how to render each particular style. Their server tells which services should be generated and in what order. Here are some widgets that the MovilePay application can display.

Widgets that the MovilePay application

Widgets in MovilePay application

Widgets in MovilePay application

Widgets in MovilePay application

So, to create a highly adaptable home screen, we had to get a response from the backend with a list of sections, each containing a list of widgets. The following is an example of a JSON response that the MovilePay application should parse and create a home screen like the example below.

Parsing and creating a home screen

[
    {
        "title": "Section 1",
        "widgets": [
            {
                "identifier": "COLLECTION_WIDGET",
                "content": [
                    {
                        "title": "Title A",
                        "image": "A",
                        "color": "yellow"
                    },
                    {
                        "title": "Title B",
                        "image": "B",
                        "color": "blue"
                    },
                    {
                        "title": "Title C",
                        "image": "C",
                        "color": "red"
                    },
                    {
                        "title": "Title D",
                        "image": "D",
                        "color": "purple"
                    },
                    {
                        "title": "Title E",
                        "image": "E",
                        "color": "green"
                    }
                ]
            },
            {
                "identifier": "IMAGES_WIDGET",
                "content": [
                    {
                        "image": "Image",
                        "color": "green"
                    },
                    {
                        "image": "Image",
                        "color": "blue"
                    },
                    {
                        "image": "Image",
                        "color": "orange"
                    }
                ]
            },
            {
                "identifier": "COLLECTION_WIDGET",
                "content": [
                    {
                        "title": "Title E",
                        "image": "E",
                        "color": "green"
                    },
                    {
                        "title": "Title F",
                        "image": "F",
                        "color": "purple"
                    },
                    {
                        "title": "Title G",
                        "image": "G",
                        "color": "red"
                    },
                    {
                        "title": "Title H",
                        "image": "H",
                        "color": "blue"
                    },
                    {
                        "title": "Title H",
                        "image": "H",
                        "color": "yellow"
                    }
                ]
            }
        ]
    },
    {
        "title": "Section 2",
        "widgets": [
            {
                "identifier": "CELLS_WIDGET",
                "content": [
                    {
                        "title": "Cell 1",
                        "color": "red"
                    },
                    {
                        "title": "Cell 2",
                        "color": "purple"
                    },
                    {
                        "title": "Cell 3",
                        "color": "yellow"
                    },
                    {
                        "title": "Cell 4",
                        "color": "blue"
                    },
                    {
                        "title": "Cell 5",
                        "color": "dark green"
                    }
                ]
            }
        ]
    }
]

We'll also go through some screens that may be built using backend-driven development.

Home screens in backend-driven development

Flexible navigation

The Super App developed by MovilePay has achieved its intended goal of being innovative. MovilePay has learned a lot from hypothesis testing, but one thing they are particularly good at is payment processing or the payment process for various services and products. They had a payment app that could process payments for any service they provided. The ability to manage payment transactions was a significant advantage, as MovilePay could drive down prices by processing multiple transactions and gaining valuable insights into consumer behavior.

MovilePay decided to develop a payment SDK based on the Google Pay, Apple Pay, or VisaCheckout model that could be integrated with any other application.

However, since working on an SDK implies very little ability to test using typical development patterns, automation is necessary.

Since MovilePay dealt with payments, the transformation of flows was critical. MovilePay couldn't afford to lose its users at any conversion funnel stage. Therefore, optimizing the entire business process was necessary — from user registration to adding a card and paying. It is where Backend-Driven Development came in handy again, turning the server into the "navigator" of the application.

Optimizing the business process with BDD

None of the MovilePay application screens knew which screen was next. After the previous screen had completed its tasks, the server was responsible for returning which screen should be displayed next.

Routes were actions that an application could recognize and respond to through a structure known as a Router. The login mechanism included two separate action branches: one for the page title and one for other elements (such as a username change or a new password) that were encountered in the action tree. The router processed them by sending them to the server, which then interpreted their actions and determined which interpretations should be on the next screen.

This modest shift in strategy allowed streamlining. MovilePay tried many different ways to display the signup form. It was possible to test whether it is preferable to show the checkout screen before or after adding the card. For example, this is one of the reasons why MovilePay was able to increase its conversion rate by 30% compared to other payment options.

Another example is how MovilePay used Backend-Driven Development to solve its problems. We think you are wondering how to apply this approach in practice. Let me show you how easy it is.

There are many alternative methods to achieve the same goal. We'll show you how MovilePay did it for iOS. If desired, this concept can be applied to any other front-end platform.

When MovilePay implemented it, they considered requirements such as ease of adding additional widgets, code readability, and single responsibility. To make things simpler and more native, MovilePay decided to use the Codable API for serialization.

Widgets (iOS)

In terms of flexibility, MovilePay felt that the best solution would be to generalize widgets that needed to be parsed with a protocol. MovilePay also sets an enum to determine which widget structure to parse the data.

protocol Widget: Decodable {}

enum WidgetIdentifier: String, Decodable {
    case banner = "BANNER"
    case collection = "COLLECTION"
    case list = "LIST"

    var metatype: Widget.Type {
        switch self {
        case .banner:
            return BannerWidget.self
        case .collection:
            return CollectionWidget.self
        case .list:
            return ListWidget.self
        }
    }
}

They are taking advantage of the Codable API through the Decodable protocol implemented by the Widget protocol. Below is an example of defining a widget structure.

struct BannerWidget: Widget {
    private let imageURLString: String
}

struct CollectionWidget: Widget {

    struct Item: Decodable {
        let imageURLString: String
        let title: String
        let subtitle: String
    }

    let sectionTitle: String
    let list: [Item]
}

struct ListWidget: Widget {

    struct Item: Decodable {
        let imageURLString: String
        let text: String
    }

    let sectionTitle: String
    let list: [Item]
}

Finally, it was defined as a type erasure, which interpreted any widget with the required initialization method, when one needs to change the custom initialization given by Decodable.

final class AnyWidget: Decodable {

    private enum CodingKeys: CodingKey {
        case identifier
    }

    let widget: Widget?

    required init(from decoder: Decoder) throws {
        do {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let type = try container.decode(WidgetIdentifier.self, forKey: .identifier)
            self.widget = try type.metatype.init(from: decoder)
        } catch {
            self.widget = nil
        }
    }
}

This erasure type is used to decrypt the Widget's identifier and its "meta-type" property to determine which widget structure should be used to parse the rest of the parsed Widget's data.

All this results in the structure below being able to parse a response containing all the information about the widgets. It has a unique feature: an array of Widget protocol types and can decrypt each Widget using the type erasure defined above.

struct HomeResponse: Decodable {

    private enum CodingKeys: CodingKey {
        case widgets
    }

    let widgets: [Widget]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.widgets = try container.decode([AnyWidget].self, forKey: .widgets).compactMap { $0.widget }
    }

    init(widgets: [Widget]) {
        self.widgets = widgets
    }
}

MovilePay could have chosen other options, such as not using the protocol and relying on the backend to return an array of each supported widget for parsing. However, we found that our protocol choice was the best choice for maintenance and readability. This approach was enough to create a new structure and add cases to the enum every time it required a new widget to be created. With a different system in a similar situation, the design of HomeResponse would have to be changed.

Below is a possible JSON API response that this model will parse.

{
    "widgets": [
        {
            "identifier": "BANNER",
            "imageURLString": "url_image_to_be_downloaded"
        },
        {
            "identifier": "COLLECTION",
            "sectionTitle": "Section Title",
            "list": [
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "title": "Title item 1",
                    "subtitle": "Subtitle item 1"
                },
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "title": "Title item 2",
                    "subtitle": "Subtitle item 2"
                },
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "title": "Title item 3",
                    "subtitle": "Subtitle item 3"
                }
            ]
        },
        {
            "identifier": "LIST",
            "sectionTitle": "Section Title",
            "list": [
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "text": "Text item 1"
                },
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "text": "Text item 2"
                },
                {
                    "imageURLString": "url_image_to_be_downloaded",
                    "text": "Text item 3"
                }
            ]
        },
        {
            "identifier": "BANNER",
            "imageURLString": "url_image_to_be_downloaded"
        }
    ]
}

This approach is very close to the development of the Super App, which allowed MovilePay to present different services to different users, test many display options, work out hypotheses, and determine which widget will be used for which service. The change in screen sorting and service grouping was close to what MovilePay had done before.

Navigation (iOS)

After fixing the widget issue, MovilePay tried to improve navigation similarly. They created the Action protocol, which was identical to the Widget protocol.

An Action is a structured object returned in the JSON response of some MovilePay APIs, with an ID and any parameters that should be displayed in the scene it represents. As a result, the Action protocol is responsible for helping to deconstruct the structured object.

protocol Action: Decodable {
    func scene() -> UIViewController
}

enum ActionIdentifier: String, Decodable {
    case home = "HOME"
    case screenOne = "SCREEN_ONE"
    case screenTwo = "SCREEN_TWO"

    var metatype: Action.Type {
        switch self {
        case .home:
            return HomeAction.self
        case .screenOne:
            return ScreenOneAction.self
        case .screenTwo:
            return ScreenTwoAction.self
        }
    }
}
view raw

The only difference between the Action protocol and the Widget protocol is that we provide a method that returns the appropriate scene for each Action in the Action definition. As an example, take a look at how these actions are implemented.

struct HomeAction: Action {
    func scene() -> UIViewController {
        return HomeCoordinator.scene()
    }
}

struct ScreenOneAction: Action {
    let title: String

    func scene() -> UIViewController {
        return ScreenOneCoordinator.scene(title: self.title)
    }
}

struct ScreenTwoAction: Action {
    let title: String
    let subtitle: String

    func scene() -> UIViewController {
        return ScreenTwoCoordinator.scene(title: self.title, subtitle: self.subtitle)
    }
}

The example above shows that, when created, Actions must contain all the necessary properties to initialize their scene and to call the Coordinators method to instantiate the UIViewController. Coordinators are structures that provide a UIViewController. Here's something to mention: MovilePay uses a design pattern in Coordinators structs, represented by a static scene() method tasked with generating a UIViewController instance for each stage.

final class HomeCoordinator: Coordinator {
    static func scene() -> UIViewController {
        // Create the ViewController and all the architecture components of this scene...
        return createdViewController
    }
}

final class ScreenOneCoordinator: Coordinator {
    static func scene() -> UIViewController {
        // Create the ViewController and all the architecture components of this scene...
        return createdViewController
    }
}

final class ScreenTwoCoordinator: Coordinator {
    static func scene() -> UIViewController {
        // Create the ViewController and all the architecture components of this scene...
        return createdViewController
    }
}

It should also be noted that, despite the chosen architectural design pattern (MVC, MVP, MVVM-C, VIPER-C, VIP, or any other architecture that uses a coordinator to create scenes and move from one scene to another), implementation using Action and Backend-Driven Development is quite appropriate.

For the Actions MovilePay context, we used the same type of erasure as for widgets, with a slight adaptation.

final class AnyAction: Decodable {

    private enum CodingKeys: CodingKey {
        case identifier
    }

    let action: Action?

    required init(from decoder: Decoder) throws {
        do {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let type = try container.decode(ActionIdentifier.self, forKey: .identifier)
            self.action = try type.metatype.init(from: decoder)
        } catch {
            self.action = nil
        }
    }
}

A pertinent note here is that MovilePay could use the Generics design pattern to avoid duplicating type erasure code. However, they chose not to. The following is an example of a structure that contains an Action.

struct ResponseModelForActions: Decodable {
    private enum CodingKeys: CodingKey {
        case action, text
    }

    let action: Action?
    let text: String

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.text = try container.decode(String.self, forKey: .text)
        let anyAction = try? container.decode(AnyAction.self, forKey: .action)
        self.action = anyAction?.action
    }
}

It can be JSON provided by an API, for example, to create a UIButton on the screen. The action object handled after the user taps this button can perform the provided action, causing the application to display the home screen.

{
    "text": "Demonstrative text",
    "action": {
        "identifier": "HOME_SCREEN"
    }
}

It was easily achieved by extending the Coordinator protocol to allow all coordinators to get a new scene via the Actions object.

It allows the coordinator to act, that is, generate the next UIViewController instance for that action and then display it.

extension Coordinator {
    func scene(using action: Action) -> UIViewController {
        return action.scene()
    }
}

A hint on server implementation

You are probably wondering what all this looks like on the server side. How not to confuse your services and core capabilities with external information? The secret to success in software development is to work in layers.

So, in addition to all the complex core services, MovilePay added another layer to the server, which, by abstracting all other services and applying all the application logic, transforms the data into the response expected by the frontend. This layer is called BFF, or Backend For Frontend is a layer that provides communication between two separate and unrelated ends of the system. It is where strings, images, streams, and style variations are configured and applied to the underlying data before sending them to applications.

Conclusions

Using the Backend-Driven approach has several advantages, which we have tried to clarify throughout the article. However, this is just another solution template. It's not a magic pill for app development. In addition, the context in which the application will be used must be considered. Do you need to create applications for multiple platforms? What types of testing would you like to do? Do you need complete control over all screens? How big will your payloads be? Do you have enough resources to carry out this project, including a development team capable of handling these tasks?

Above all else, you should always be careful about what level of flexibility you need. Creating extremely versatile UI components or using non-standard fonts and margins can make the codebase incredibly complex, leading to a worse user experience.

Most projects don't need this kind of flexibility. When choosing a Backend-Driven approach, it is essential to consider whether you have the resources in the form of finance and a development team to develop and maintain such a project and whether you can correctly calculate the payload of your application.