# Query planning
The gateway plans subgraph calls for each operation, fetches any required entity fields, and combines the results into a GraphQL response.
# Explain a query plan
Use explain to see which subgraphs a query will call without executing it. For the lookup example below:
val plan = interpreter.explain("""
query {
product(id: "p1") {
name
reviews { body }
}
}
""")
A plan for this query could look like:
query
fetch catalog at $.product fields [name, id (key)]
fetch reviews after catalog at $.product via Product(id) fields [reviews.body]
Each fetch names a subgraph. $.product is the location in the client response. (key) marks a field needed for a later lookup, even if the client did not request it. after catalog means that reviews must wait for the catalog result.
# Connecting objects across ordinary services
Federation schemas already explain how to fetch an entity from another service. With ordinary GraphQL services, you provide that information using a Lookup.
Suppose the catalog service exposes this schema:
type Query {
product(id: ID!): Product
}
type Product {
id: ID!
name: String!
}
The reviews service adds reviews to Product and exposes a batch lookup:
type Query {
productsByIds(ids: [ID!]!): [Product!]!
}
type Product {
id: ID!
reviews: [Review!]!
}
type Review {
body: String!
}
Use the reviews schema above as reviewsSdl, and describe how the service fetches products:
import caliban.gateway.{ Lookup, Subgraph }
import zio.http._
val reviews = Subgraph
.graphql("reviews", url"http://reviews:8080/graphql", reviewsSdl)
.withLookup(
Lookup.list(
"Product",
"productsByIds",
"ids" -> Lookup.Argument.batch(Lookup.Argument.key("id"))
)
)
Compose the catalog and reviews subgraphs. A client can then ask:
query {
product(id: "p1") {
name
reviews { body }
}
}
The gateway fetches product from the catalog, including id even though the client did not request it. It then calls productsByIds(ids: ["p1"]) on reviews and attaches the returned reviews to that product. The lookup belongs on the service being called, which is reviews here.
Use Lookup.single when the subgraph fetches one object at a time. Use Lookup.list when it accepts several keys in one request:
- Results can arrive in any order: the gateway matches each returned object to its key through the key fields. Return non-null objects and omit missing ones.
Argument.key("id")reads theidfrom the object that the gateway is fetching.Argument.obj(...)builds an input object for the subgraph.Argument.batch(...)builds one argument value for each requested object.Lookup.listreads keys only insideArgument.batch, andLookup.singledoes not accept it. The compiler rejects both mistakes.
For a service that exposes productById(id: ID!): Product, use:
Lookup.single(
"Product",
"productById",
"id" -> Lookup.Argument.key("id")
)
Prefer a batch lookup wherever the subgraph supports one. It collapses several objects into a single subgraph request.
# Operation cache
The gateway reuses validated operations and query plans for repeated requests. Subgraph calls still fetch current data. Set the total estimated cache weight with GatewayConfig.withMaxOperationCacheWeight:
val bounded = gateway.withConfig(_.withMaxOperationCacheWeight(8L * 1024L * 1024L))
If you use Configurator.setValidations, reuse the same validation functions across requests. Creating fresh lambdas prevents cache reuse.
Document resolution runs before cache lookup. Authorization runs before execution, including on cache hits. A resolution hook can disable caching for a request with cacheable = false.
# Supported operations
The gateway supports queries, mutations, subscriptions, and deferred fragments. It does not support @stream. It never forwards @defer or @stream to subgraphs.
# Deferred fragments
@defer is always enabled. The gateway first sends the fields outside deferred fragments, then one payload per deferred fragment. Payloads use the same format as Caliban's own @defer support: multipart/mixed with deferSpec=20220824.
Each deferred fragment is fetched in one of these ways:
- Fields from another subgraph are fetched after the initial payload.
- Fields of an entity from the same subgraph get a second call that looks the entity up by its key, so a slow field does not hold up the initial payload.
- A fragment with fields of an object the gateway cannot look up by key is not deferred: those fields are fetched with their parent, so the whole fragment arrives in the parent's payload.
@deferis only a hint, and this keeps the errors and nulls of those fields in the payload that carries them. - A deferred fragment on root query fields gets its own root call.
explain lists each deferred fragment with the calls it makes:
query
fetch products at $.product fields [name, id (key)]
defer reviews at $.product fields [reviews.body]
fetch reviews after products at $.product via Product(id) fields [reviews.body]
A field selected both inside and outside a deferred fragment arrives in the earlier payload. Under a list, each item gets its own entry with its index in path. A non-null violation inside a deferred fragment stops at the fragment: its entry has data: null and the error.
Mutation and subscription root fields cannot use @defer, and subscriptions reject it anywhere. Deferred fields below a mutation field are fetched after the mutation completes, so the mutation runs once.
The request timeout covers deferred fragments, and shutdown waits for them like any other request. A fragment that does not finish in time gets data: null and a timeout error. The operation, execution, and completion hooks end with the initial payload. Calls made for deferred fragments still run the subgraphCall and attempt hooks.