GraphQL Server in Java, Part II: Understanding Resolvers
Learn more about resolvers in GraphQL and Java.
Join the DZone community and get the full member experience.
Join For FreeIn part one, we developed a simple GraphQL server. That solution had one serious flaw: All fields were loaded eagerly on the back-end, even if they weren't requested by the front-end. We sort of accept this situation with RESTful services by not giving clients any choice.
RESTful API always returns everything, which implies always loading everything. If, on the other hand, you split RESTful API into multiple smaller resources, you risk N+1 problem and multiple network round trips. GraphQL was specifically designed to address these issues:
- 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
You may also like: Moving Beyond REST With GraphQLRESTful APIs make arbitrary decisions regarding how much data to return. Therefore, one can hardly ever fix the aforementioned issues. It's either over- or under-fetching.
OK, that's theory, but our implementation of the GraphQL server doesn't work this way. It still fetches all the data, irrespective of whether it was requested or not. Sad.
Evolving Your API
To recap, our API returns an instance Player
DTO:
@Value
class Player {
UUID id;
String name;
int points;
ImmutableList<Item> inventory;
Billing billing;
}
That matches this GraphQL schema:
type Player {
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
}
By carefully profiling our application, I realized that very few clients ask for 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;
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)
);
}
Fields like 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 {
id: String!
name: String!
points: Int!
inventory: [Item!]!
billing: Billing!
trustworthiness: Float!
}
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:
- 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 a new schema. This is OK. Clients are unaware oftrustworthiness
so they don't request it. - You distributed a 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:
com.nurkiewicz.graphql.Player.trustworthiness()
com.nurkiewicz.graphql.Player.getTrustworthiness()
com.nurkiewicz.graphql.Player.trustworthiness
This happens on the 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 percent 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.
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 the 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;
import org.springframework.stereotype.Component;
@Component
class PlayerResolver implements GraphQLResolver<Player> {
}
Keep it empty, the GraphQL engine will guide us. When trying to run the application one more time, the exception is familiar, but not the same:
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 the 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:
@Component
class PlayerResolver implements GraphQLResolver<Player> {
float trustworthiness(Player player) {
//slow and painful business logic here...
return 42;
}
}
Of course, in the real world, the implementation would do something clever. Take a 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 the Player
may be delegated to the resolver. The schema remains the same, but the Player
class becomes hollow:
@Value
class Player {
UUID id;
}
So how does GraphQL know how to calculate name
, points
, inventory
, billing
, and trustworthiness
? Well, there is a method on a resolver for each one of these:
@Component
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) {
//...
}
}
The implementation is unimportant. What's important is laziness: These methods are only invoked when a certain field was requested. Each of these methods can be monitored, optimized, and tested separately. Which is great from a performance perspective.
Performance Problem
Did you notice that inventory
and billing
fields are unrelated to each other? For example, 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 installment. Stay tuned!
Further Reading
GraphQL Java Example for Beginners Using Spring Boot
Published at DZone with permission of Tomasz Nurkiewicz, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments