Federated GraphQL @ Walmart
By: Nidhi Sadanand
Tags: Engineering, GraphQL, Microservices

Federation Directives @extends & @requires

@extends

Before we dive in, we will touch upon what this directive does. @extends is a GraphQL directive specified at the type level that indicates that the entity is a remote entity that is being referenced in the current service. A remote entity , if we talk in equivalent Rest terms, is a an entity that you can fetch from a service. So in that sense , the entity also needs to specify the contract of HOW to fetch it. This is where we specify the key fields on the remote entity that dictate the contract with the remote service for fetching that entity. So Module entity that is extended from the ViewModule service can look like this. To fetch the Module, one would need to provide the moduleId as marked by @external

extend type ViewModule @key(fields:"moduleId") {

moduleId: String! @external }

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

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

Decorating an extended entity



productId: String @external //populated by the Product Service

canAddToCart: boolean // decorated by CartService on Product entity

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

Extended entity as input for another field



productId:String

}

type Cart {

items:[Product]

} 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 functiontype Cart {items:[Product]

@requires

@requires is used to specify fields needed internally for business logic. The required field might not be queried for by the client, but is needed by the current service to resolve the field on which it is defined. For example, if we need to tag a specific type of modules as top modules, then topModules is a field that you can add to the ViewModel to implement this functionality where you have the @require instruction to get all the modules before filtering. Clients will only ask for the topModules and not modules . Also modules would not be part of the Remote Entity’s key but it is there in its fields.



pageId : String

modules: [Module] //needed for field below

topModules: [Module]

} extend type View @key (fields: "pageId") {pageId : String @external modules: [Module] //needed for field belowtopModules: [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

Unions cannot be extended from a remote service. They can however be packaged and shared across different services that will need the same signature.

Query Plan execution

The Gateway analyses the queries and then prepares the orchestration path. For this it analyses dependencies between the services as specified by @extends and @requires . All fetch calls to the dependencies are then “hoisted” before the calling the service. While this is the most obvious & best approach to ensuring services that have dependencies on other parts of the graph get those resolved before the service is called, where it can get tricky is when for performance reasons (say service SLAs are high) you want to start the call to your service resolution BEFORE needing the dependencies, so that they are executed in parallel.

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.



extend type Reviews

productId: String

reviews: [Review]

} //Review Serviceextend type Reviews @key (fields: "productId") {productId: String @external reviews: [Review]

type Product

productId:String

productId: String

url: String

fulfilmentInfo:FulfilmentInfo

} //Product Servicetype Product @key (fields: "productId") {productId:StringproductId: Stringurl: StringfulfilmentInfo: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

To be able to quickly identify the pattern of GQL that was being used by various services, we found it useful to associate a name to these various patterns that we have listed here.

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

Federated GraphQL works well for separation of concerns and autonomy of teams. However some use cases have to be modelled keeping in mind the above considerations such as Shared Entities, dependencies between nodes of the graph which can lead to inefficiencies both from a performance point of view as well as schema maintenance point of view.

Thanks to Naga Malepati for contributing and reviewing this article.