GraphQL server in Java: Part II: Understanding Resolvers
- fetch only required data to avoid extra network traffic as well as unnecessary work on the backend
- allow fetching as much data as needed by the client in a single request to reduce overall latency
Evolving your API
To recap our API returns an instancePlayer
DTO:@Valuethat matches this GraphQL schema:
class Player {
UUID id;
String name;
int points;
ImmutableList<Item> inventory;
Billing billing;
}
type Player {By carefully profiling our application I realized that very few clients ask for
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
}
billing
in their queries, yet we must always ask billingRepository
in order to create Player
instance. A lot of eager, unneeded work:private final BillingRepository billingRepository;Fields like
private final InventoryClient inventoryClient;
private final PlayerMetadata playerMetadata;
private final PointsCalculator pointsCalculator;
//...
@NotNull
private Player somePlayer() {
UUID playerId = UUID.randomUUID();
return new Player(
playerId,
playerMetadata.lookupName(playerId),
pointsCalculator.pointsOf(playerId),
inventoryClient.loadInventory(playerId),
billingRepository.forUser(playerId)
);
}
billing
must only be loaded when requested! In order to understand how to make some parts of our object graph (Graph-QL! duh!) loaded lazily, let’s add a new property called trustworthiness
on a Player
:type Player {This change is backwards compatible. As a matter of fact, GraphQL doesn’t really have a notion of API versioning. What is the migration path then? There are a few scenarios:
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
trustworthiness: Float!
}
- you mistakenly gave new schema to clients without implementing the server. In that case, the client fails fast because it requested
trustworthiness
field that the server is not yet capable of delivering. Good. With RESTful API, on the other hand, the client believes the server is going to return some data. This can lead to unexpected errors or assumptions that the server intentionally returnednull
(missing field) - you added
trustworthiness
field but did not distribute new schema. This is OK. Clients are unaware oftrustworthiness
so they don’t request it. - you distributed new schema to clients once the server was ready. Clients may or may not use new data. That’s OK.
trustworthiness
, but it doesn’t know how to calculate it when asked. Is this even possible? NO:Caused by: [...]FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:This happens on startup of the server! If you change the schema without implementing the underlying server, it won’t even boot up! This is fantastic news. If you announce that you support certain schema, it’s impossible to ship an application that doesn’t. This is a safety net when evolving your API. You only deliver schema to clients when it’s supported on the server. And when the server announces certain schema, you can be 100% sure it’s working and properly formatted. No more missing fields in the response because you are asking the older version of the server. No more broken servers that pretend to support certain API version, whereas in reality, you forgot to add a field to a response object.
com.nurkiewicz.graphql.Player.trustworthiness()
com.nurkiewicz.graphql.Player.getTrustworthiness()
com.nurkiewicz.graphql.Player.trustworthiness
Replacing eager value with lazy Resolver
Alright, so how do I add trustworthiness
to comply with the new schema? The not-so-smart tip is right there in the exception that prevented our application to start. It says it was trying to find a method, getter or field for trustworthiness
. If we blindly add it to the Player
class, API would work. What’s the problem then? Remember, when changing the schema, old clients are unaware of trustworthiness
. New clients, even aware of it, may still never or rarely request it. In other words, the value of trustworthiness
needs to be calculated for just a fraction of requests. Unfortunately, because trustworthiness
is a field on a Player
class, we must always calculate it eagerly. Otherwise, it’s impossible to instantiate and return response object. Interestingly with RESTful API, this is typically not a problem. Just load and return everything, let clients decide, what to ignore. But we can do better.First, remove
trustworthiness
field from Player
DTO. We have to go deeper, I mean lazier. Instead, create the following component:import com.coxautodev.graphql.tools.GraphQLResolver;Keep it empty, GraphQL engine will guide us. When trying to run the application one more time, the exception is familiar, but not the same:
import org.springframework.stereotype.Component;
@Component
class PlayerResolver implements GraphQLResolver<Player> {
}
FieldResolverError: No method or field found as defined in schema [...] with any of the following signatures [...], in priority order:
com.nurkiewicz.graphql.PlayerResolver.trustworthiness(com.nurkiewicz.graphql.Player)
com.nurkiewicz.graphql.PlayerResolver.getTrustworthiness(com.nurkiewicz.graphql.Player)
com.nurkiewicz.graphql.PlayerResolver.trustworthiness
com.nurkiewicz.graphql.Player.trustworthiness()
com.nurkiewicz.graphql.Player.getTrustworthiness()
com.nurkiewicz.graphql.Player.trustworthiness
trustworthiness
is looked for not only on the Player
class, but also on PlayerResolver
that we just created. Can you spot the difference between these signatures?PlayerResolver.getTrustworthiness(Player)
Player.getTrustworthiness()
Player
as an argument whereas the latter is an instance method (getter) on Player
itself. What is the purpose of PlayerResolver
? By default, each type in your GraphQL schema uses default resolver. That resolver basically takes an instance of e.g. Player
and examines getters, methods and fields. However, you can decorate that default resolver with a more sophisticated one. One, that can lazily calculate field for a given name. Especially when such field is absent in Player
class. Most importantly, that resolver is only invoked when the client actually requested said field. Otherwise, we fall back to default resolver that expects all fields to be part of the Player
object itself. So how do you implement a custom resolver for trustworthiness
? The exception will guide you:@ComponentOf course, in the real world, the implementation would do something clever. Take a
class PlayerResolver implements GraphQLResolver<Player> {
float trustworthiness(Player player) {
//slow and painful business logic here...
return 42;
}
}
Player
, apply some business logic, etc. What’s really important is that if the client doesn’t want to know trustworthiness
, this method is never called. Lazy! See for yourself by adding some logs or metrics. That’s right, metrics! This approach also gives you great insight into your API. Clients are very explicit, asking only for necessary fields. Therefore you can have metrics for each resolver and quickly figure out, which fields are used and which are dead and can be deprecated or removed. Also, you can easily discover which particular field is costly to load. Such fine-grained control is impossible with RESTful APIs, with their all-or-nothing approach. In order to decommission a field with RESTful API, you must create a new version of the resource and encourage all clients to migrate.Lazy all the things
If you want to be extra lazy and consume as little resources as possible, every single field of thePlayer
may be delegated to the resolver. The schema remains the same, but the Player
class becomes hollow:@ValueSo how does GraphQL know how to calculate
class Player {
UUID id;
}
name
, points
, inventory
, billing
and trustworthiness
? Well, there is a method on a resolver for each one of these:@ComponentThe implementation is unimportant. What’s important is laziness: these methods are only invoked when certain field was requested. Each of these methods can be monitored, optimized and tested separately. Which is great from a performance perspective.
class PlayerResolver implements GraphQLResolver<Player> {
String name(Player player) {
//...
}
int points(Player player) {
//...
}
ImmutableList<Item> inventory(Player player) {
//...
}
Billing billing(Player player) {
//...
}
float trustworthiness(Player player) {
//...
}
}
Performance problem
Did you notice thatinventory
and billing
fields are unrelated to each other? I.e. fetching inventory
may require calling some downstream service whereas billing
needs an SQL query. Unfortunately, GraphQL engine assembles response in a sequential matter. We’ll fix that in the next instalment, stay tuned!- Part I: Basics
- Part II: Understanding Resolvers
- Part III: Improving concurrency
- github.com/nurkiewicz/graphql-server-demo