Federated GraphQL @ Walmart

Nidhi Sadanand
Dec 31, 2020 · 10 min read
Trail in Bangalore — from my iPhone

This post talks about our motivation (Walmart Customer Experience) to adopt Federated GraphQL and some of the use cases & patterns that we saw with migrating REST based Orchestrators to Federated GraphQL. This assumes you are familiar with GraphQL semantics in general, but might be new to the Federation Specification.

Introduction

Motivation

Domain Model Consistency

Developer Ergonomics

Performance

Measuring Success against Goals

  • Client Payload size reduction — we found around 60% improvement in payload sizes for the same functionality
  • Number overlapping calls to the same domain service — 100% improvement as all calls went via a single service or the gateway
  • Reusability of domain entities from FE & BE — 100% schemas were extended for additional functionality. Additionally GQL ensures that there is only one resolver a type.

Learnings with Federated Graphs

Shared Entities

//Types in ViewModule Service
interface ModuleConfigs {
configs: JSON
}
type ViewModule {
name: String!
type: String!
version: Int!
configs: ModuleConfigs!
}type P13NModuleConfig implements ModuleConfigs {
recommendations: [ProductInfo]
metaData:MetaData
}
type BannerModuleConfig implements ModuleConfig {
bannerImgUrl:String
bannerText:String
}
type ContentLayout {
layout: JSON
modules: [ViewModule]
}
extend type query {
contentLayout:ContentLayout
}
//Queryquery HomeLayout {
contentLayout {
layoutJSON,
modules {
moduleName
...on BannerModuleConfig {
bannerImageUrl
}
...on P13NModuleConfig {
product {
productId
description
url
}
}
}
}
//Search ViewModel Types

type GuidedNavConfig implements ModuleConfig {
config: JSON
guidedNavigation: [NavigationItems] //comes from Search Response
}
//SearchView Query
searchView {
layoutJSON,
modules {
moduleName
...on GuidedNavConfig {
guidedNavigation {
navigationItems {
itemName
link
}
}
}
}
}

A straight forward GQL schema would first get the modules and then enrich the modules from the federated services. This approach however does not work in the case of search because the modules and search results are closely tied together and the querying for the search results is the long pole of the orchestration and we did not want any added latency in first fetching the modules and subsequently enrichment with search results.

Our initial version of Federation, resulted in separation based on Domains. So ViewModels for Search was driven from Search Service (whose primary responsibility is to serve search results given query). In Search, the clients need both the Modules on the page as well as the results of the search query. There are a number of modules on the Search Page that are dependent on the Search Results (Such as facets or filters, or guided navigation). We needed to be able to orchestrate for search view in CMS and search results in parallel and then stitch together the view from the results. This requirement created the following issues

  • Backend entry to the ViewModel service using schema stitching techniques such as delegateToSchema
  • Different queries from client for a Search View vs any other view to facilitate the request going to search

To over come these and make the code more maintainable, we made the ViewModel Service as a mini Orchestrator talking to the Personalisation Service, Content Management System AND the Search Backend . The next section talks about why this pattern (different domains controlling the aggregate result) is a hard proposition to model in GQL.

Federation Directives @extends & @requires

@extends

extend type ViewModule @key(fields:"moduleId") {
moduleId: String! @external
}

There are two reasons why a dependency to this module is specified

  1. When you need to decorate the module to add functionality to it
  2. When you use an remote entity in as a composed field in a type

Decorating an extended entity

extend type Product @key(fields: "productId") {
productId: String @external //populated by the Product Service
canAddToCart: boolean // decorated by CartService on Product entity
}

Extended entity as input for another field

extend type Product @key(fields: "productId") {
productId:String @external //indicates that the service extending this entity, will provide the productId to resolve a Product using __resolveType function
}
type Cart {
items:[Product]
}

@requires

extend type View @key(fields: "pageId") {
pageId : String @external
modules: [Module] //needed for field below
topModules: [Module] @requires (fields:"modules { moduleId description config { configJSON }")
}

If Module is a complex field composed of other types, then the requires directive will need to de-structure the complex object to specify the exact fields needed. This can go out of hand very quickly with the amount of boiler plate extensions that will need to be done.

Unions & Interfaces

Query Plan execution

To illustrate, a Product has a fulfilment attribute which comes the Fulfilment Service and takes 400ms to get the response. We have a reviews attribute which extends Product Schema and is populated by the reviews service. To do this it needs the productId from the Product Service. Because the Parent Resolver and Fulfilment resolver are both in the Product Service, and the Query Plan optimises on service hops, so the Gateway in this case will wait all of attributes from Product Service before making the call to Reviews Service. So while reviews does not need fulfilment info, it has to wait for the long tail of Product resolution before the call to reviews can happen.

//Review Service
extend type Reviews @key(fields: "productId") {
productId: String @external
reviews: [Review]
}
//Product Service
type Product @key(fields: "productId") {
productId:String
productId: String
url: String
fulfilmentInfo:FulfilmentInfo
}

The above indicates that having too many dependencies for a schema increases the boiler plate that one has to write in federated services and the dependencies affects the query execution plan — hence the hard problem of modelling a performance optimised view model for the Search View

Patterns of Service Integration

Atomic Entity

Simplest integration where a service does not have any other dependencies with other parts of the graph.

Atomic Entity Representation

Mini Orchestrator

Here the GQL service orchestrates directly with other services due to the fact that it has a complex upstream dependencies for resolving its schema.

Mini orchestrator Entity Representation

Aggregated Entity

Aggregated entities reference remote entities and aggregate it their types , they need to do this because they either need to pre-process before resolving to the remote entities.

Aggregated Entity Representation

Extended Entity

An extended entity decorates a remote type by adding fields to it.

Extended Entity Representation

Conclusion

Thanks to Naga Malepati for contributing and reviewing this article.

Walmart Global Tech Blog

We’re powering the next great retail disruption.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store