I’m working with two federated subgraphs:
• Subgraph A defines an entity EntityA.
• Subgraph B defines an entity EntityB.
In subgraph A, EntityA references EntityB through a field field1: EntityB. In subgraph B, EntityB has a field field2: Int.
The challenge: subgraph A has logic to determine whether field2 should be removed for a specific EntityB, but field2 is defined in subgraph B.
I’ve considered several approaches, but each feels problematic:
Adding a flag (e.g., removeField2) to @key() on EntityB so the entity resolver can omit field2. The downside is that all other subgraphs need to know about removeField2 (since key should be the same).
Adding an EntityA resolver in subgraph B that uses an removeField2 @external flag. However, this forces subgraph B to be aware of subgraph A’s existence, which breaks the intended separation.
Creating an additional subgraph that somehow manages the logic between subgraph A and subgraph B — though I’m unsure how best to do that.
I also attempted using the @provides directive, but that’s static. I need a scenario where, if removeField2 is true (known by subgraph A), we omit the field, while otherwise letting subgraph B resolve it.
Has anyone dealt with a similar “conditional field removal” scenario in a federated setup? Any best practices or patterns you could suggest?
Unsure what you mean by “removal of a field”, but if I understand correctly, EntityA is a parent of EntityB and has some logic within subgraph A (based on information on EntityA) that determines whether EntityB.field2 should be NULL or not, i.e.
// subgraph A
type EntityA @key(fields: "id") {
id: ID!
// some other fields
}
// subgraph B
type EntityA @key(fields: "id") {
id: ID!
field1: EntityB
}
type EntityB @key(fields: "id") {
id: ID!
field2: String // value based on some conditional logic
}
If I understand correctly, you should be able to achieve t his through @context (docs), i.e.
// subgraph B
type EntityA @key(fields: "id") @context(name: "myContext") {
id: ID!
field1: EntityB
// this field would be computed in subgraphA
shouldFieldBeNull: Boolean! @external
}
type EntityB @key(fields: "id") {
id: ID!
field2(overwrite: Boolean! @fromContext(field: "$myContext { shouldFieldBeNull }")): String
}
Thank you for your reply! I’m trying to understand how subgraph B references subgraph A’s internal logic. Wouldn’t this create a coupling between the two subgraphs and potentially violate the principle of separation of concerns? Or is it acceptable for subgraph B to know about subgraph A in a GraphQL federation context?
Per your problem description, in order to resolve EntityB.field2 you do need some information from subgraph A so there is no way around it. Regardless whether it is across subgraphs or within the same graph, your EntityB needs input from the parent EntityA.
Yes, I see your point. Another option might be to add a shouldFieldBeNull flag directly to EntityB and include it as part of EntityB’s key. That way, subgraph B wouldn’t need to know anything about subgraph A. With this approach, any subgraph could decide to remove the field if necessary—although most likely they won’t need to.
However, this solution is also problematic because it forces every subgraph that references EntityB to handle the shouldFieldBeNull field, even though it’s optional. This can create unnecessary complexity in subgraphs that don’t need this logic.
I’d argue that adding unnecessary elements to the @key field set is an anti pattern. @key should specify all the necessary information to uniquely identify the entity.
We recommend using @requires (information from same entity but other subgraph) and/or @context (ancestor information) directives to propagate additional information to the specific resolvers.
@requires seems promising. However, consider a scenario where subgraph C also references EntityB but has no need to remove any fields. For instance, if we do something like:
// subgraph A
type EntityA @key(fields: "id") {
id: ID!
field1: EntityB
}
type EntityB @key(fields: "id") {
id: ID!
shouldRemoveField2: Boolean @inaccessible
}
// subgraph B
type EntityB @key(fields: "id") {
id: ID!
shouldRemoveField2: Boolean @external
field2: Int @requires(fields: "shouldRemoveField2")
}
// subgraph C
type Query {
someC: EntityC
}
type EntityC @key(fields: "id") {
id: ID!
field1: EntityB
}
type EntityB @key(fields: "id") {
id: ID!
}
It becomes impossible to execute the query { someC { field1 { field2 } }}, because subgraph B requires shouldRemoveField2, while subgraph C does not provide it.
A possible workaround is present in your first answer:
// subgraph A
type Query {
myA: EntityA
}
type EntityA @key(fields: "id") {
id: ID!
shouldRemoveField2: Boolean @inaccessible
field1: EntityB @shareable
}
type EntityB @key(fields: "id", resolvable: false) {
id: ID!
}
// subgraph B
type EntityA @key(fields: "id") {
id: ID!
shouldRemoveField2: Boolean @external
field1: EntityB @requires(fields: "shouldRemoveField2") @shareable
}
type EntityB @key(fields: "id") {
id: ID!
field2: Int
}
Here, I’m unsure of the query plan for { myA { field1 { field2 } }}. Will the router call the entity resolver for EntityB, or will it call the resolver for EntityA to fetch field2?
Query planner will “jump” to other subgraphs through _entities call to get additional entity fields.
Given your above examples 1, 2 and 3
#1 This works as long as the shouldRemoveField2 is resolvable based on EntityB information only in subgraph A (its computed without any knowledge about its parent),
i.e. given your query someC { field1 { field2 }}, generated query plan will be
#2 and #3 will not work as you cannot have only one subgraph specifying requirement on the shareable field. Even if you remove the EntityA.field1 from subgraphA this still is problematic as your requirement is on specific field resolution path (i.e. it assumes that you would “enter” EntityB through that EntityA.field1 field on subgraphB … but if you add another subgraph where you extend EntityB it may jump there through _entities query.