Implementing Server-Driven UI Architecture on the Shop App

Located within the Shop organization at Shopify, the Shop Store team is responsible for managing the buyer experience on the Shop App’s Store Screen—the screen where buyers can view a merchant’s products and collections. We recently implemented an architecture called server-driven UI that allows us to control which store sections we want to display for a merchant’s store. 

Prior to implementing server-driven UI, the display logic for the Store Screen was purely client-driven. In the context of a client-server architecture, this meant that the Shop client side was responsible for handling all aspects of the UI, including the layout of the Store Screen, visual design elements, and which UI components to render. 

As part of my third placement in the Dev Degree program, I joined the Shop Store team as a Mobile Developer Intern. Within this role, I got the opportunity to co-champion the building of a new Store Screen using a server-driven UI architecture. This experience taught me how to design and implement a scalable mobile design pattern, and ship high-quality production code. I also overcame technical challenges such as deciding between an existing team’s server-driven UI components or creating Shop Store-specific components, and figuring out how to support layouts or sections that older client versions were unfamiliar with. This post discusses how my team implemented the server-driven UI architecture on the Store Screen, the lessons I learned, and challenges I overcame while building this architecture.

Why Server-Driven UI Makes Sense

Previously with the client-driven display logic, the Store Screen rendered the same store layout and components for all stores, regardless of the size of the merchant or the number of products available:

The default Store Screen layout prior to Server-Driven UI.
The default Store Screen layout prior to Server-Driven UI

A problem that emerged with the client-driven architecture was that the store layouts couldn’t be customized based on the needs of individual merchants. For instance, for merchants with many products, it would be appropriate to display a “Best Sellers” section, since a small subset would be expected to sell better than the rest. However, for merchants with very few products, a standalone “Best Sellers” section doesn’t make sense since it would duplicate the main product listing.

Given the nature of a static, client-driven UI, we were limited on the frequency and timing in which new experiments could launch since we were bound by a weekly release cadence. The server-driven UI architecture, on the other hand, allows us to launch experiments whenever we deem necessary, and to run these experiments across different templates and layouts for merchants. This is made possible by the back end being able to control which experiments are currently being run, and which templates and layouts are to be shown for a merchant.

Another issue with the client-driven implementation for the Store Screen was the timeline required to push fixes if rendering issues were discovered after a release had been pushed to users. Since the release process for the Shop App occurs weekly, any fixes for client side took up to a week to go live.

In the server-driven UI architecture, the server side is responsible for informing the client which store sections (for example: Best Sellers, New Arrivals, All Products) and layouts (Grid or Shelf) to render. The main objective of this server-driven UI architecture is enabling merchant stores to be personalized based on their varying needs. This would be performed through templated store layouts, which would render sections needed by the merchant. 

This architecture also allows the team to conduct several experiments at once, for several merchants, in order to better understand their behavior. Over time, these experiments allow the team to make the best version of the store. 

Furthermore, the server-driven UI architecture also ensures certain aspects of the store are not dependent on the app releases. For example, if there’s an issue with one of the sections being rendered on a store, a change on the back end can be implemented to ensure that section isn’t displayed for merchants, and the issue can be fixed for a future app release.

The product design images below show the default template for all merchants, with some variations. The “Best Sellers” section is rendered as a grid of products with a large display type. The Store Screen also renders an “All Products” section that displays a grid of products with a small display type, and a “Shop All Products” action, which takes buyers to a screen with all products. Another variation of the default template is a store that has collections, which are a grouping of products belonging to a category. The collections belonging to a store are rendered as a shelf of products with a small display type, and a “View all” action, that navigates buyers to another screen that displays all of the collections. 

Product Grid with large display type and View All action.
Product Grid with large display type and “View All” action.
Product Grid with small display type and Shop All Products action.
Product Grid with small display type and “Shop All Products” action.
CollectionShelf with small display type and View All action.
CollectionShelf with small display type and “View All” action.

A Closer Look at the Architecture

The architecture for the server-side is made up of the template processing layer, orchestrator, data loading layer, and shop-server (previously known as Arrive-Server) GraphQL layer. 

The server-side architecture for server-driven UI.” style=

The server-side architecture for server-driven UI.

Here’s a breakdown of each layer:

Template Processing Layer: Contains a hard-coded template, which describes the type of sections included in the default template for a store. This layer also has customizations from merchants that specify what they want visible on their store. The Template Metadata Reader reads in the static templates and merchant-specific templates from the Merchant Database, and returns the sections to be rendered.

Data Loading Layer: Creates a SectionDataLoader object for each section to be rendered, after being provided with the section needed. This SectionDataLoader object contains all of the logic for fetching the section data. 

GraphQL Layer: Maps the SectionDataLoader objects to the corresponding GraphQL types, and resolves the types and fields for the Shop App. 

Orchestration Layer: Orchestrates the entire flow between the Template Processing Layer, Data Loading Layer, and GraphQL layers, and passes in the expected information for each layer.

On the server side, the GraphQL API allows a store sections query to be performed, which returns a list of sections for the shop store. Each section is built from a base Section type, and a section can be further defined as a ProductsSection or CollectionsSection. The ProductsSection type is utilized for sections that return a list of products, such as “All products”, “Best Sellers”, or “Trending”. On the other hand, the CollectionsSection type is utilized for sections that return a list of collections. Various layout types that have configurable size properties such as GridLayout and ShelfLayout were also defined. The grid layout supports a 2x2 grid with large-sized product blocks or 3x3 grid with medium-sized product blocks. The shelf layout permits users to scroll to the right, and supports small or large product blocks. Having this layout property on a section allows us to easily create new ways to display entities without having to create a brand new section.

On the client side, the rendering of components is handled by the following component architecture:

The client-side architecture for server-driven UI.” style=
The client-side architecture for server-driven UI.

We first have a top-level component called ServerDrivenStoreScreen, that buyers are navigated to when they want to view a merchant’s store. For each section in the Store Screen (“Collections”, “Best Sellers”, “All Products”), we render the StoreSectionContainer component. Along with rendering this component, the ServerDrivenStoreScreen also renders the store header, navigation bar, and product search bar.

The StoreSectionContainer then determines which section to render, based on the section type provided to it. If the section type is a product, then the StoreSectionContainer will render the ProductSection component. Otherwise, if the section type is a collection, then the StoreSectionContainer will render the CollectionsSection component. The ProductsSection component will render the ProductShelf or ProductGrid component based on the display type passed from the server. The ProductsSection component also provides the callback function to the ProductGrid / ProductShelf component that navigates to the ProductDetails page when a product is pressed.

The CollectionsSection component renders a CollectionShelf component to display collections of products. This component also provides the callback function to the CollectionShelf component that navigates to the CollectionDetails screen when a collection is pressed. The ProductGrid component renders a grid of products, and also supports both large and small display sizes. Lastly, the ProductShelf and CollectionShelf components both render a shelf of products or collections that can be scrolled to the right. 

In order to obtain all of the data required for the components, there will be two primary GraphQL calls that will be made. The first GraphQL call is a StoreSections query that fetches the StoreSections data for rendering all of the store’s sections. This call is made in the ServerDrivenStoreScreen using the useStoreSections hook, and the returned store sections data is passed down to the various sub-components.

The second GraphQL call is an existing ShopInfo query that pre-fetches the data from cache for populating the store header. This call is made in the ServerDrivenStoreScreen using the useStoreInformation hook, and the StoreHeader component is rendered in the ServerDrivenStoreScreen.

Learning Through Impact in Dev Degree

As a Dev Degree Intern, I worked on building the ProductGrid, ProductsSection, and StoreSectionContainer components, and creating the StoreSections query and useStoreSections hook. Working on components from various layers of the architecture allowed me to deeper understanding of the Shop client architecture. I was also able to draw upon the React knowledge I obtained from previous Dev Degree placements, and apply them towards this project.

In addition to the client work I worked on, I also contributed toward key decision-making such as determining whether to reuse an existing team’s server-driven UI components or creating Shop Store-specific components. In order to overcome this challenge, my team and I created a pros and cons list for both approaches. We made the decision to create our own Shop Store components when needed, since this would allow us to control the data types in each section, and to reuse any components that could be easily decoupled. 

Another challenge my team and I overcame was figuring out how to support layout types or section types older client versions are unfamiliar with. We approached this problem by, once again, creating a pros and cons list that analyzed all the benefits and fallbacks from a back-end and client-side standpoint. In the end, we decided that the client would be responsible for deciding how to handle the layout/sections provided from the query. The client would define a default layout for each section, and this default layout would be rendered if the client receives a new layout type it does not understand.

Overall, implementing a server-driven UI puts merchants in the driver’s seat in terms of how they want to personalize their store. The architecture also enables Shop to tailor the shopping experience for both new and returning buyers and provide a more individualized experience. This provides the opportunity to find out what engages buyers in various contexts. Server-driven UI also unlocks several new capabilities such as the ability to run different experiments in different templates at the same time and update the UI for buyers without having to release a new app version. As a developmental team, this ensures that the team has a system that can grow with the product over time. We hope that this server-driven UI architecture can be leveraged and widely implemented by other teams across Shop. 

I want to thank the Shop Store developmental team for their constant support and technical knowledge throughout this project. As a Dev Degree Intern, this experience of leading the client work enabled me to take on a greater technical responsibility on my team, and helped me hone my technical abilities, and project management skills. This experience shaped me into a better developer, and I’m looking forward to applying my learnings not only in future placements at Shopify, but also towards my Computer Science degree. 

Ashwin Narayanan is a 4th year intern in the Dev Degree program and 4th year Computer Science student at Carleton University. You can follow his work on GitHub.

Earn a computer science degree through a learning and placement path that’s unlike any other computer science program. Dev Degree is an innovative evolution of the traditional co-op program, allowing you to master development skills faster. Hands-on experience at Shopify in parallel to attending classes gives you the opportunity to put those skills to use on real-world problems—while you learn. Sound interesting? Learn more and apply here.