Aug 15, 2026 16 min readBy Haranadh Gavara

When GraphQL Filtering Becomes Query Planning: Connected Knowledge Across Subgraphs

A comment on my previous article prompted me to look more closely at a problem I had initially judged too quickly. The question arose in an architecture where H...

#Knowledge Graphs#Enterprise Architecture#AI Governance#RAG

A comment on my previous article prompted me to look more closely at a problem I had initially judged too quickly.

The question arose in an architecture where Hot Chocolate and Fusion are used as the GraphQL entry point, and filtering becomes difficult once the underlying data is connected across relationships and services.

My first reaction was straightforward: this looks like a GraphQL filtering problem.

Hot Chocolate already provides filtering, sorting, pagination and projection capabilities, and its filtering model can translate GraphQL filter arguments into native database queries. It can also be extended with custom providers and handlers when the default data-provider model is insufficient.

So my initial thought was that the answer would probably be somewhere around custom filters, resolvers or gateway configuration.

A deeper look changed my view.

The difficult part is not always expressing the filter.

The real challenge begins when different parts of the filtering condition belong to different connected data sources.

At that point, filtering starts becoming query planning.


This Is Not Only a Fusion Problem

I will use Hot Chocolate and Fusion throughout this article because that is the stack that triggered the discussion.

The architectural problem itself is broader.

A similar architecture can be built using Apollo Federation, where a federation router sits in front of independently owned GraphQL subgraphs. Apollo also maintains compatibility specifications for subgraphs implemented using different server technologies.

Conceptually, both architectures lead to something similar:

Client → Federation Gateway → Multiple Domain Subgraphs

The important question is not whether Fusion or Apollo Federation is used.

The question is:

What happens when the filter that determines which entities belong in the result depends on data owned by several subgraphs?

That is where API federation begins to meet distributed data processing.


What Hot Chocolate and Fusion Already Solve

Before discussing the difficult case, it is important to establish what Hot Chocolate and Fusion already give us.

Hot Chocolate handles the GraphQL server concerns: schemas, filters, sorting, pagination, projections, resolvers and extensibility around the underlying data provider.

Fusion operates at another level. ChilliCream describes Fusion as a gateway that exposes a single GraphQL API across multiple upstream services. It composes their contracts and orchestrates their execution at runtime. Those upstream services can include GraphQL, OpenAPI REST and gRPC services.

A simplified Fusion architecture therefore looks like this.

GraphQL Client → Fusion Gateway → Research, People, Funding and Knowledge subgraphs

For normal distributed field resolution, this model works well.

Suppose a Work is returned by the Research subgraph and the GraphQL query asks for additional information about its Author. That Author information may be owned by the People subgraph. Fusion can coordinate the required execution and assemble the final response.

The interesting problem begins when Author information is no longer something we simply want to display.

It determines whether the Work should have been returned in the first place.

That distinction between field resolution and candidate selection is the central issue in this article.


A Running Example

To make the problem concrete, consider a research knowledge graph with the following simplified model:

Now consider this query:

Find research works published since 2024, authored by researchers affiliated with the University of Oxford, funded by Wellcome, and related to Knowledge Graphs.

The query is intentionally illustrative.

What matters is that the answer depends on several connected relationships.


When One Graph Engine Owns the Data

If all the required information is available inside one graph database, this is not particularly difficult.

The GraphQL layer can translate the request into the native graph-query language and push the complete predicate into the graph engine.

GraphQL Request → Hot Chocolate → Graph Query Translation → Neo4j / RDF Store

The resulting architecture is conceptually simple:

The important point is not whether the backend happens to be Neo4j, an RDF store, or another graph database.

The important point is that one query engine can see the complete connected predicate.

It can evaluate:

before sorting and pagination are applied.

That is exactly where the query should ideally execute.

In this situation there is little reason to recreate graph-query capabilities inside the GraphQL gateway.

Let the graph database do the graph work.


Now Split the Same Knowledge Across Subgraphs

The architecture becomes more interesting when ownership is distributed.

Imagine:

The GraphQL query has not changed.

But its predicates now belong to different systems:

Each individual condition is straightforward.

The difficult part is the AND between them.

Four independently owned predicates must jointly determine one sorted and paginated candidate set.

No individual subgraph owns:

This was the point at which my initial assumption that I was looking at a GraphQL filtering problem no longer held.


Field Resolution Is Not Candidate Selection

Consider two apparently similar requests.

The first is:

This is primarily field resolution.

The second is:

Now Oxford affiliation determines the candidate set.

The same is true for Wellcome funding and Knowledge Graph classification.

Fusion can orchestrate execution across upstream services, but the architecture still needs somewhere to decide which Works satisfy the complete cross-domain predicate before global pagination is finalised.

That small semantic difference substantially changes the execution problem.


Why Fetch First and Filter Later Breaks Down

The simplest implementation might be:

For small result sets, that may be perfectly acceptable.

But let's consider the following scenario

Suppose the GraphQL client requests the first 20 matching Works. Research returns 20 recent Works. After checking Oxford affiliation, Wellcome funding and topic classification, perhaps only three survive.

Should the API return three?

If the client actually asked for the first 20 matching Works, another page has to be retrieved. And perhaps another after that.

Sorting causes the same difficulty.

We cannot reliably say that we have the first 20 matching Works ordered by publication date until the complete filtering condition has been taken into account.

Filtering, sorting and pagination are therefore no longer independent.

The architecture needs to establish the candidate set.

That is where filtering begins turning into query planning.


This Felt Familiar From Spark

At this point, the problem started feeling surprisingly familiar from my work with Spark.

In distributed processing, evaluating a simple predicate is rarely the expensive part.

Moving data can be.

Spark's own tuning guidance emphasises serialisation, network performance, and memory usage because distributed execution costs are heavily influenced by what moves between processes and nodes.

The same instinct applies here.

If four subgraphs independently produce large collections of IDs and the gateway pulls them all into memory solely to intersect them, the gateway begins to behave like a poorly designed distributed-processing driver.

The architectural instinct I would bring from Spark is therefore:

Reduce early. Push computation towards the data. Move the smallest possible intermediate result.

That principle becomes important in evaluating the possible solutions.


Option 1: Push the Query Into One Graph Engine

The first solution is also the simplest.

If the required knowledge can reasonably be represented within a single graph backend, push the complete predicate there.

The graph database already has indexes, traversal capabilities and its own query optimizer.

There is little value in recreating those capabilities above it.

This is the architecture I would choose whenever the complete connected predicate naturally belongs to one graph.


Option 2: Introduce a Context/Search Service

If ownership must remain distributed, I would not immediately build a distributed query planner.

A much more practical option is a dedicated Context/Search service.

Fusion Gateway → Context/Search Subgraph → OpenSearch Materialised Context Index → Matching IDs → Fusion Hydration

Instead of joining multiple operational systems at query time, maintain a searchable projection containing the dimensions required for candidate selection.

For the research example:

OpenSearch can then evaluate combinations of exact-value filters and return the matching documents or entity IDs. Its Query DSL explicitly distinguishes query and filter contexts, with filter context intended for exact matching without relevance-score computation.

The application-specific part can remain relatively small:

This is attractive because each technology continues to do what it is already good at.

Hot Chocolate handles GraphQL.

Fusion handles federation and distributed response composition.

OpenSearch handles indexed candidate retrieval.

Our custom component primarily translates the connected filtering intent into the context index.


But the Context Index Introduces Eventual Consistency

There is an important trade-off.

The context index is not necessarily the authoritative source.

Suppose an Author changes affiliation from Oxford to Cambridge.

The People service may contain the new affiliation immediately, while the context index still says Oxford until the corresponding change propagates.

For a short period:

Candidate selection can therefore be stale.

This is a classic read-model trade-off: optimising the query path by maintaining a materialised representation means the architecture must also manage propagation, freshness and rebuilding of that representation. OpenSearch itself supports index transformations and scheduled transformation workflows, although an event-driven architecture would normally require application-level change propagation as well.

For some knowledge queries, several seconds of eventual consistency may be perfectly acceptable.

For authorisation, compliance or operational decision-making, it may not be.

That requirement has to be explicit.


Option 3: Introduce a Canonical Query Model

Another problem appears if the platform needs to support different graph technologies.

If GraphQL filters are translated directly into Cypher, the GraphQL layer becomes coupled to Neo4j.

If they are translated directly into SPARQL, it becomes coupled to RDF.

Instead, we can introduce a small intermediate representation:

The internal representation of the running query could look conceptually like:

The Neo4j adapter translates that intent into Cypher.

The RDF adapter translates it into SPARQL.

The search adapter translates it into the appropriate OpenSearch query.

There is a custom implementation here, but it is focused on something the application genuinely owns:

the meaning of a knowledge query independent of the database that executes it.


Before Building a Planner: Try Directional Lookups

There is also an interesting middle ground between a materialised Context Index and a full distributed planner.

Instead of asking every subgraph for its complete candidate set, we can execute the predicates sequentially.

For example:

Oxford Authors → Recent Works by those Authors → Wellcome-funded Works → Knowledge Graph topic

Suppose the People subgraph tells us that only 300 Author IDs currently satisfy the Oxford condition.

Instead of retrieving every recent Work, we could ask Research:

Research now produces a significantly smaller Work set.

That smaller set can then be supplied to Funding.

Funding reduces it further.

Knowledge performs the final reduction.

Conceptually:

The Spark parallel is useful here as well.

The idea resembles choosing a small relation and using it to constrain a much larger computation, rather than forcing both large datasets through an expensive distributed movement. The exact execution mechanics differ, but the design instinct is similar: the order in which data is reduced matters. Spark's tuning documentation similarly emphasises network and memory costs as key considerations in distributed processing.

The limitation is obvious. The supposedly small ID set actually has to be small.

Sending several million Author IDs in an IN predicate merely shifts the problem elsewhere.


Option 4: Build a Distributed Planner

There is one more difficult case. The knowledge cannot be consolidated. A materialised context index is not acceptable.

Directional lookups are insufficient.

And predicates genuinely need to execute against multiple live systems.

We are now moving towards a distributed query planner.

Predicate Decomposition → Selective Subgraph Execution → Candidate Intersection → Global Sort and Pagination

A naive implementation could evaluate all predicates independently:

and then send everything to the gateway.

Technically, that works.

Architecturally, it is dangerous.


The ID Explosion Problem

Imagine transferring hundreds of thousands of IDs over the network simply so that the gateway can calculate:

The GraphQL gateway now has to allocate memory for those candidate sets, perform the intersections and retain enough state to sort and paginate the result.

We have effectively moved part of a distributed database engine into the API tier.

This is exactly where my Spark experience makes me uncomfortable.

A gateway collecting massive intermediate datasets feels very similar to pulling distributed data back towards the driver for an operation that should have remained close to the distributed execution layer.

A better strategy is usually to identify the selective predicate first and use it to constrain the more expensive queries.


Cost-Based Thinking

Suppose:

Why begin with 500,000 Works?

We might instead start from Wellcome.

Then push that smaller candidate set into Research.

Then apply Oxford.

Then Knowledge.

Distributed query engines already use this class of optimisation.

Trino's dynamic filtering documentation describes using the smaller side of a join to derive runtime filters that can reduce processing on the larger side. It also notes that its cost-based optimiser uses table statistics to choose the smaller build side.

Trino also supports predicate pushdown through connectors, allowing filtering to occur within the data source rather than after unnecessary rows have already been moved into the query engine.

Once our architecture starts making these decisions, however, we need more than a GraphQL filter translator.

We need concepts such as:

At that point, we are genuinely designing a query optimiser.


Existing Technology Can Help, But Does Not Remove the Problem

We do not need to invent the principles behind this architecture.

Apache Calcite provides an extensible query-processing framework in which optimiser rules, adapters, cost models, and statistics can participate in producing efficient plans. Its planning model explicitly supports cost-based optimisation and extensible operators and rules.

Trino provides another mature example with distributed execution, connector-level pushdown, statistics, cost-based optimisation, and dynamic filtering.

But neither is a drop-in implementation of:

Introducing a full distributed query engine simply to solve one class of GraphQL filtering problem could create far more complexity than it removes.

So I would treat a custom distributed planner as the last option, not the first.


One More Complication: Authorization

Candidate selection cannot be separated from authorization.

Suppose the Funding subgraph contains grants that only certain users or tenants are allowed to see.

A Context service cannot first select:

and apply permissions later.

The actual predicate is closer to:

Otherwise, restricted entity IDs can leak into intermediate candidate sets even if the final GraphQL response later removes them.

In a real enterprise implementation, candidate selection therefore needs access to the relevant authorisation context, or each authoritative source must enforce those rules while evaluating its part of the predicate.

This makes the identity, tenant, and policy context part of query execution, not merely an API gateway concern.


How Far Can Hot Chocolate and Fusion Take Us?

After looking at these alternatives, I think Hot Chocolate and Fusion can take the architecture quite far.

Hot Chocolate can provide the GraphQL contract and the normal filtering, sorting, pagination and data-provider extensibility required at the subgraph level.

Fusion can provide a unified GraphQL API, contract composition, and runtime orchestration across the underlying services.

If one backend owns the complete connected predicate, this may be almost everything required.

The special case starts when:

the predicate itself becomes distributed.

At that point, the architecture has to decide where candidate selection belongs.

That is not something a GraphQL framework can make disappear.


Putting It Together: Hot Chocolate + Fusion

For the architecture that triggered this investigation, I would currently put the pieces together like this.

Client → Fusion Gateway → Hot Chocolate Domain Subgraphs → Context/Search Service → Graph/Search Backends

The concrete technology stack could be:

The important architectural decision is that Fusion remains the federation layer rather than becoming the distributed database. Fusion continues doing composition and orchestration, while candidate selection is delegated to a specialised Context service when the normal subgraph execution path is insufficient. Fusion's current architecture is explicitly designed to compose upstream contracts and orchestrate their execution, while Hot Chocolate provides filtering and data-access extension points beneath individual GraphQL services.

The Context service would first attempt the cheapest available strategy. It could push the complete predicate into Neo4j when a single graph contains the relevant knowledge, use OpenSearch when a context projection exists, or perform a directional lookup when a subgraph can cheaply produce a selective candidate set.

Only queries that genuinely cannot be handled through those paths should require more sophisticated distributed planning.

That keeps the custom implementation relatively small.


The Same Architecture Without Hot Chocolate or Fusion

The underlying pattern does not need to depend on .NET.

Client → Federation Router → Polyglot Domain Subgraphs → Context Query Service → Graph/Search/Data Backends

A technology-neutral implementation could look like:

Apollo's Federation specification deliberately separates the federated graph from the implementation technology of individual subgraphs, and Apollo documents a range of compatible server implementations.

So the architecture is fundamentally:

Hot Chocolate and Fusion are one very good concrete implementation of that pattern.

They are not what creates the underlying query-planning problem.


Where I Landed

I started this investigation looking for a better GraphQL filtering solution.

I ended up seeing a spectrum of architectures.

If the connected knowledge lives in a single graph store, push the complete query to the graph engine.

If the information remains distributed but the filtering dimensions can be projected, a Context/Search service backed by an indexed read model appears to be a practical option with relatively little custom implementation.

If one predicate is highly selective, directional lookups may solve the problem without introducing another major platform component.

If multiple graph and search technologies must be supported, a canonical knowledge-query model with adapters becomes useful.

Only when predicates genuinely need to execute against multiple live systems would I consider a cost-aware distributed planner.

And even then, my experience with Spark leaves me with one strong architectural instinct:

Do not turn the GraphQL gateway into the driver that collects everything. Reduce the data where it lives, move the smallest possible intermediate representation, and introduce distributed movement only when genuinely necessary.

I am deliberately not presenting this as a finished architecture.

I would be particularly interested in hearing from teams that have encountered this problem in production.

Did you consolidate searchable knowledge into one graph?

Did you maintain a materialised context index?

Did you use directional lookups?

Did you introduce a dedicated query service?

Or did you eventually build a distributed planner?

There may be a much simpler pattern, or another important trade-off, that I have missed. That discussion would help further improve this architecture.

This investigation has also put one capability on my radar for the SolidState Graph beta: a lightweight Knowledge Query/Context layer between the API and graph adapters.

My current inclination is to start with query adapters, graph pushdown, and context indexing, and let real workloads tell us whether anything more sophisticated is actually necessary.

Author Note & Context

Authored by Haranadh Gavara, Enterprise Architect and AI Systems Leader with extensive experience directing scale engineering organizations (ex-SVP, VP, Acting CTO). Sharing operational perspectives on AI architecture, semantic data foundations, and governance-first delivery.

Back to All Articles Subscribe on LinkedIn