Versions
When discussing GraphQL Federation, it is most commonly understood that there are two versions of the protocol: Version 1 (V1) and Version 2 (V2). On the surface level, this is indeed true. However, it may be more accurate to reimagine this dichotomy as a trichotomy. But to explain why, we first need to take a glance at Federation history and some of the most important differences between versions.
What is GraphQL Federation Version 1 (V1)
Although it is probably older as a concept, the Federation package was made public in 2019, with 1.0 released at the end of 2020. While V1 contains many of the concepts familiar to users of modern Federation, it was much more rigid and inflexible. Let's take a look.
What is an entity?
Many of the differences between V1 and V2 revolve around the backbone of any federated graph, namely, "the entity". If you are not sure what an entity is, you can read about them in depth here. However, a simple definition might be the following:
- An entity is a GraphQL Object type that defines a "@key" directive.
- The "@key" directive requires the "fields" argument, the value of which corresponds to one (or more) fields defined on that object.
- A "@key" directive states that the subgraph in which this object is defined includes an "entity reference resolver" for this primary key.
- This entity reference resolver accepts the primary key as an input and is able to uniquely identify an object based on this primary key.
- This allows for the resolving of an object over multiple services.
For example, imagine the following subgraphs named "users" and "posts" respectively:
And the client makes this query:
In simplified terms, the users service sends the primary key (in this case id
) to the posts service for each User
entity. The posts service is then able to use this id to identify the specific user that needs to be resolved and return the relevant data (in this case hasPosts
) for that specific user.
Origin and extension entities
You may have noticed in the example schemas above that the users subgraph defines an Object type definition
for User
while the posts subgraph defines an Object type extension
.
In V1, there exists the concept of "origin entities". Each entity must exist as a type definition (the origin) in exactly one subgraph; and all other definitions in any other subgraphs must be defined as extensions.
Unfortunately, some GraphQL providers did/do not support "extension orphans" (extensions that are defined without a base definition in the same subgraph). Consequently, a workaround is provided through the @extends
directive. Where such limitations are imposed, the entity extension can be defined thus:
An extension or an @extends
directive makes it clear that an entity is not the origin.
In addition to this extension requirement, any primary keys must be defined as @external
. However, an important note is that key fields that are defined @external
on an extension are resolvable by that subgraph, unlike the usual interpretation of @external
, i.e., "this field or type is not resolvable by this subgraph".
Entity field contribution
While entity primary key fields can be defined across multiple subgraphs with @external
, other non-key fields can be contributed without the @external
directive. However, any such contributed fields must not be shared; they can only be defined in one subgraph. In the example schemas above, hasPosts
exists on User
in only the posts subgraph. It is not part of a primary key, so it cannot be included in another subgraph. Consequently, the posts service must have sole responsibility for resolving the hasPosts
field.
Entity stubs
If an entity does not contribute non-key fields, it is considered an "entity stub". Stubs provide the minimal data to identify an entity so that other fields defined in other subgraphs can be resolved. For instance, consider the following subgraphs named "api" and "users" respectively.
And this query:
Here, the entity stub User
defined in the api subgraph provides exactly enough data to identity each user through the entity reference resolver in the users subgraph. This entity "jump" provides the operation entry point (Query.accounts
) defined in the api subgraph a route to resolve the User.name
field from the users subgraph.
Value types (shared types)
A "value type" is a type that is defined in more than one subgraph and has no "origin"; instead, each subgraph in which the value type is defined shares ownership for that type. Enums, Input Objects, Interfaces, Objects*, Scalars, and Unions can each be value types.
*Except entities, which cannot be value types, because non-key fields must be defined only once.However, in V1, value types must be identical across the subgraphs that define them. For example, consider the following subgraph schema:
Each subgraph that defines MyEnum
must only define the enum values A
and B
. Similarly, each subgraph that defines MyObject
must also only define the fields age
and name
. Moreover, each field's return type, nullability, and defined arguments (if any) must also be identical; e.g., a object field that returns String
would be incompatible with another instance of that field that returns String!
.
Shareability
The @shareable
directive was introduced in V2 and does not exist in V1. However, something extremely important to note is that all V1 GraphQL Object fields are considered to be intrinsically @shareable
. This is a pivotal feature that allows V1 subgraphs to be compatible with V2 subgraphs.
@shareable
here.@external directive
As previously mentioned, entity key fields that are declared @external
on a type extension will still be considered resolvable from that subgraph. However, any other @external
declarations will be considered unresolvable from that subgraph. Moreover, in V1, there are very few rules dictating where and how @external
can be used. The @requires
and @provides
directives do not require their field set references to be declared @external
in V1 (changed in V2).
@external
here.What is GraphQL Federation Version 2 (V2)
The Graphql Federation Version 2 alpha was announced in late 2021 with 2.0 released the following year. One of the main intentions of V2 was to enhance the developer experience. V2 introduced simpler syntax, and more flexibility, in addition to important measures both to encourage cooperation across teams and protect against subgraph changes that could lead to unintended consequences (more on this in another post).
What causes a subgraph to be interpreted as a V2 subgraph?
The rules that govern whether a subgraph is interpreted to be a V2 subgraph a fairly straight forward:
- The subgraph defines an
@link
directive that imports version2.0
or higher. - The subgraph defines/uses at least one V2 specific directive, e.g.,
@override
or@inaccessible
(each V2 directive will have its own dedicated learn page).
If a subgraph includes one (or both!) of these features, the subgraph will be interpreted to be a V2 subgraph (and V2 composition rules will apply to it).
Origin entities
The concept of an "origin entity" no longer exists in V2. The requirement of special syntax (extend
keyword/@extends
directive and @external
key fields) to define an entity in more than one subgraph is no longer necessary (but still permitted for backwards compatibility). Consequently, each entity can simply be defined as a type definition, regardless of whether it contributes new fields or acts as an entity stub. Moreover, the restriction that non-key entity fields must only be defined in a single subgraph is removed.
@external directive
V2 composition tightens the rules behind valid use of @external
. Types and fields that are defined @external
must conform to one of the following rules:
- The field is a key field on an entity (an object that defines a
@key
directive). - The field or type is "provided" through a
@provides
directive on at least one path. - The field or type is "required" through a
@requires
directive on at least one path. - The field exists on its type to satisfy an interface.
As in V1 (and to support backwards compatibility), key fields that are declared @external
on entity extensions will be considered resolvable from the current subgraph.
Failure to conform to one of these rules will produce a composition error.
You can find an in-depth discussion of@external
here.Value types (shared types)
The restriction that value types must be identical in all subgraphs that define them is removed in V2. However, there are still some special composition rules (of particular note for enums, which you can read about here) that govern whether different value types are compatible with one another. For example, consider the following two subgraphs:
When composed, the federated graph would look like so:
Although the users service returns String!
from User.name
, the api service returns String
. Consequently, the least restrictive return type will be the type that appears in the federated graph (to support possible nullability from the api service). However, each instance of a single shared field must be a superset (equally or less restrictive) of the most restrictive return type. For instance, while [Int]
and [Int!]!
would be compatible with one another, [Int!]
and [Int]!
would not be compatible (and would result in a composition error).
Furthermore, should, for example, an Object type be differently defined across subgraphs, all fields defined on that object, no matter the subgraph in which the field is defined nor the operation path that requests the field, must be fully resolvable at all times.
For an in depth discussion on field resolvability, see this page.FieldSet validity changes
The are (currently) three directives that use the FieldSet
scalar: @key
, @provides
, and @requires
. A FieldSet
is essentially a GraphQL selection set without the first set of encompassing curly braces ({
and }
). In V1, a FieldSet
could define top-level composite types. For example:
However, in V2, composite types within a FieldSet
without selections are invalid and will result in a composition error. Each and every selection within a FieldSet
must end with a leaf type (an Enum or a Scalar). Consequently, this would be a breaking change when composing a V1 subgraph that defines such a FieldSet
with V2 composition. So, to fix the schema above, we would now be required to select at least one leaf on User.account
. For example:
The "third" GraphQL Federation version
Up to this point, we've discussed the GraphQL Federation version dichotomy. So what is this elusive "third version"? This is a concept to which we at WunderGraph refer as "Version 1.5".
When Federation Version 2 was announced, it was advertised to be backwards compatible, requiring no major changes to existing subgraphs. But as we have already discussed, V1 was much more rigid and inflexible than V2. So, in order to accommodate for the disparity of rules (and their strictness), some compromises had to be made. This might be broken down like so:
"True" Version 1
A subgraph that is compatible with the routers and gateways that existed before (or do not support) the concept of GraphQL Federation Version 2.
Version 1.5
A subgraph that is interpreted to be a V1 subgraph by a V2 router/gateway but would not be compatible with non-V2 router and gateways.
Version 2
A subgraph that is interpreted to be a V2 subgraph by a V2 router/gateway.
An example of Version 1.5 subgraphs
Consider the following two (composable) subgraphs named "users" and "posts", respectively:
Both of these subgraphs would be interpreted to be V1 subgraphs for the following reasons:
- No
@link
directive that imports from2.0
or higher. - No usage of "V2-only" directives, e.g.,
@inaccessible
.
You may also have noticed that User.details
does not require the @shareable
directive to successfully compose, which corresponds with rules that all V1 fields are intrinsically considered to be @shareable
. However, these subgraphs also exhibit features that would not be permissible under strict V1 composition rules. For instance:
- The
User
entity defines a non-key field in more than one subgraph (User.details
). - The
Details
object is a value (shared) type but is not identically defined across subgraphs. - The
Region
enum is a value (shared) type but is not identically defined across subgraphs. - Both subgraphs appear to define an "origin entity"; i.e., neither subgraph defines an entity extension nor any
@external
key fields.
So, while these two subgraphs would be interpreted to be V1 subgraphs by V2 composition, they would not be compatible with V1 composition. What results is a sort of hybridization that is not quite V1 and not quite V2; hence, "V1.5".
Conclusion
If you're using a router/gateway that supports GraphQL Federation Version 2 (such as WunderGraph Cosmo), you probably don't need to worry about your V1-interpreted subgraphs' being "true" V1 subgraphs. However, if you ever needed to use or test against legacy software and versions, it is important to bear in mind that V1-interpreted subgraphs that V2 composition deems valid are not necessarily valid with "true" V1. So, in certain cases, modifications to subgraphs would be necessary for V1 composition to be successful.
On the other hand, it is always necessary to understand when and why a subgraph would be interpreted as either V1 or V2. When migrating a V1 subgraph over to V2, the simple addition of a V2-only directive could break composition due to rules surrounding @shareable
and @external
. And in some cases, a V1 subgraph that was compatible with V1 composition might need tweaks to be composable with V2 composition (for example, a FieldSet
that defines composite types without leaf selections).