Subgraph dependencies in Apollo Federation using @requires and @external
This is the fourth part in a series of blog posts on the topic of Apollo Federation, and how we use it at Volvo Car Mobility. If you want to read more, make sure to check out our other posts on the topic.
In this post, the topic will be subgraph dependencies in Apollo Federation. The blog post assumes some basic knowledge of Apollo Federation.
When can subgraph dependencies appear?
A federated GraphQL setup is a great way to separate the logic and code of individual domains to allow teams to work autonomously and limit the number of teams having to work in the same code repositories. But a world, where each domain and team never have to consider each other, is not realistic. There will always be some dependencies between services and domains.
Let’s look at an example; the following Car
type has a field hourlyPrice
that is based on the model of the Car:
type Car {
id: ID!
model: String!
hourlyPrice: Int!
}
But the prices belong to a different domain, which in turn means a different service and subgraph. Using federation, we want the car and price subgraphs to resolve their own fields, the federated schema would look like the following:
# Car Subgraph
type Car @key(fields: "id") {
id: ID!
model: String!
}
# Price Subgraph
type Car @key(fields: "id") {
id: ID!
hourlyPrice: Int!
}
But hourlyPrice
is dependent on model
, so there is now a subgraph dependency between the price subgraph and car subgraph. One way to solve it would be to have the car subgraph resolve hourlyPrice
by requesting the hourlyPrice
of the model
from price-service. Alternatively, we could do the other way around, price-service resolves hourlyPrice
by sending a request to car-service for the model
of a specific id
.
Both alternatives mean we have to create and maintain specific endpoints for resolving other services’ fields. Furthermore, the perk of Federation where a subgraph only needs to care about fields related to its domain is lost. This pattern could quickly become messy and hard to maintain. Luckily Apollo Federation has support for subgraph dependencies using the @requires
and @external
directives.
@external
and @requires
in Apollo Federation
Let’s quickly go over the definitions of the directives from the Apollo Federation documentation:
@external
Indicates that this subgraph usually can’t resolve a particular object field, but it still needs to define that field for other purposes.
@requires
Indicates that the resolver for a particular entity field depends on the values of other entity fields that are resolved by other subgraphs.
These two work well together to solve the problem of subgraph dependencies. With these, we can tell the federation gateway that we have a field that requires the value of another field, but we cannot resolve the field ourselves.
If we want to rewrite the price subgraph schema using these directives it would look like the following:
# Price Subgraph
type Car @key(fields: "id") {
id: ID!
model: String! @external
hourlyPrice: Int! @requries(fields: "model")
}
This means that if someone is querying hourlyPrice
the gateway have to provide the value of the field model
before it can be resolved by the price subgraph. In practice, the federation gateway will first query the Car subgraph for the id and model field, then execute an entities query (for more information about entities see one of our previous blog posts) where the value of model is passed via the representations
query variable like this:
{
"representations": [
{
"__typename": "Car",
"id": "1"
"model": "Volvo V60"
}
]
}
The @external
field could also be a parent type from which you want to get a value, like in the following example:
# Price Subgraph
type Car @key(fields: "id") {
id: ID!
model: Model! @external
hourlyPrice: Int! @requries(fields: "model { priceCategory }")
}
type Model {
name: String
year: Int
priceCategory: String
}
In this case, we would use the directives in the same manner, and just dig a bit deeper into the representations variable that is passed from the gateway:
{
"representations": [
{
"__typename": "Car",
"id": "1"
"model": {
"priceCategory": "C"
}
}
]
}
Implementing a DGS resolver for a field with @requires
Let’s take a look at how to implement a resolver for the first hourlyPrice
example defined above. We need to extract the value of model
from the representations
variable to resolve hourlyPrice
. In a DGS resolver, this would look like the following:
@DgsComponent
class CarResolver(
val priceService: PriceService
) {
@DgsEntityFetcher(name = "Car")
...
@DgsData(parentType = "Car")
fun hourlyPrice(representation: Map<String, Any>): Int {
val model = representation["model"] as String
return priceService.getHourlyPriceForCarModel(model)
}
}
We implement the hourlyPrice
in the reference resolver of the CarType. By adding an argument of type Map<String, Any>
to the function DGS will populate it with the representation of the entity we are currently resolving, that is the element in the $representations
list that corresponds to the entity. We extract the value of model from the map and use this to get the price.
For the second example in the previous section with an object as the external field, we simply parse another Map from the representation passed to the function:
@DgsComponent
class CarResolver(
val priceService: PriceService
) {
@DgsEntityFetcher(name = "Car")
...
@DgsData(parentType = "Car")
fun hourlyPrice(representation: Map<String, Any>): Int {
val model = representation["model"] as Map<String, Any>
val priceCategory = model["priceCategory"] as String
return priceService.getHourlyPriceForPriceCategory(priceCategory)
}
}
Final Words
Subgraph dependencies are inevitable in a federated architecture, and it can be easy to default to requests between microservices to solve them. With @requires
and @external
, you can define your dependencies directly in the schema, and let the Federation gateway handle the additional requests for you. This results in less and more maintainable code, and a better separation of concern, where each subgraph can focus solely on the fields it is responsible for. This is one of the big advantages of working with Apollo Federation.
If you want to read more about our work with Federation at Volvo Car Mobility, make sure to check out the other blog posts we wrote on the topic.
Join the movement
If this blog post made you interested in working with Apollo Federation at Volvo Car Mobility, make sure to explore our careers page for open roles.
Written by Christopher Gustafson, Iman Radjavi and Alexander Lindquister.