Overview
Let's continue building our accounts
subgraph! We have our list of types and fields that we need to migrate from the monolith
subgraph. But how do we do this safely?
In this lesson, we will:
- Learn about the
@override
directive - Learn about the progressive
@override
approach for incremental migration - Start to migrate our
accounts
subgraph types
The @override
directive
To migrate fields safely from one subgraph to another, we use the @override
directive.
We can apply @override
to fields of an entity and fields of root operation types (such as Query
and Mutation
). It tells the router that a particular field is now resolved by the subgraph that applies @override
, instead of another subgraph where the field is also defined.
The @override
directive takes in an argument called from
, which will be the name of the subgraph that originally defined the field.
Let's take the Query.user
field as an example, which currently lives in the monolith
subgraph. We want to move it over to the accounts
subgraph. In the accounts
schema, we would add the directive just after the return type of the field:
type Query {user(id: ID!): User @override(from: "monolith")}
The monolith
subgraph schema can stay the same, but the router won't call on it anymore when the user
field is requested.
Incremental migration with progressive @override
In production environments, we probably want to take it slower, monitoring performance and issues as we make these changes. For example, we might want to send only 10% or 25% of all operations towards the accounts
subgraph. If all goes well and there are no issues, we can bump that number up incrementally, until we get to the full 100% migration.
To do this, we can add another argument to the @override
directive: label
. This argument takes in a string value starting with percent
followed by a number in parentheses. The number represents the percentage of traffic for the field that's resolved by this subgraph. The remaining percentage is resolved by the other (from
) subgraph.
Let's examine the Query.user
field again. If we wanted the accounts
subgraph to handle only 25% of traffic, we would annotate it like so:
type Query {user(id: ID!): User @override(from: "monolith", label: "percent(25)")}
The monolith
subgraph schema still stays the same, but now the router will route its requests to the monolith
subgraph 75% of the time, and the accounts
subgraph 25% of the time for the Query.user
field.
Let's migrate!
Here's our list of types and fields for the accounts
subgraph again. We'll take them one by one, doing the first two together types together. Then, you'll get a chance to do the rest on your own!
User
interface withid
,name
,profilePicture
Host
type withid
,name
,profilePicture
andprofileDescription
Guest
type withid
,name
,profilePicture
Query.user
Query.me
Mutation.updateProfile
- The types used by the
updateProfile
mutation:UpdateProfileInput
,UpdateProfileResponse
andMutationResponse
The User
interface
Let's start with the User
interface. We can find its definition in the monolith/schema.graphql
file. Copy and paste it into the accounts
subgraph schema.
"Represents an Airlock user's common properties"interface User {id: ID!"The user's first and last name"name: String!"The user's profile photo URL"profilePicture: String!}
What about in the monolith
subgraph? Well, we still need to keep the User
interface defined there. We reference it as a return type for the Review.author
field, and we also implement the interface for the Host
and Guest
types.
However, we don't need to keep all the extra fields that are the responsibility of the accounts
subgraph! We can remove both name
and profilePicture
.
"Represents an Airlock user's common properties"interface User {id: ID!- "The user's first and last name"- name: String!- "The user's profile photo URL"- profilePicture: String!}
We do still need to keep the id
field, because that's the bare minimum we need to identify a specific instance of a user (like a stub!)
Interface definitions can be shared between subgraphs by default, and those definitions can differ in each subgraph. This is how we can remove the name
and profilePicture
fields from the User
interface definition in the monolith
subgraph!
Now that we've got our User
interface squared away in both subgraphs, let's move on to the implementing types, Host
and Guest
.
✏️ The Host
entity
Let's first take a look at the Host
type and its fields. For each field, we've added a comment indicating which subgraph should be responsible for that field, based on the separation of concerns principle.
type Host implements User @key(fields: "id") {id: ID! # the key field, a unique identifier for a particular instance of a Hostname: String! # accounts subgraphprofilePicture: String! # accounts subgraphprofileDescription: String! # accounts subgraphoverallRating: Float # reviews subgraph, eventually}
Using the plan above, we'll need to do the following:
- Migrate three fields (
name
,profilePicture
andprofileDescription
) over to theaccounts
subgraph, which will be responsible for contributing those fields to theHost
entity. - The
monolith
subgraph will keep contributing theoverallRating
field (eventually, thereviews
subgraph will, but let's take it one step at a time!).
Let's get to it!
Tip: Try using side-by-side windows for this process and throughout the course. We'll be splitting off types and fields from the monolith
subgraph schema (on the left), and duplicating them over to the accounts
subgraph schema (on the right).
From the
monolith
subgraph schema, let's copy theHost
type, its key field and the three fields it'll be responsible for (name
,profilePicture
andprofileDescription
) over to theaccounts
subgraph schema.subgraph-accounts/schema.graphqltype Host implements User @key(fields: "id") {id: ID!"The user's first and last name"name: String!"The user's profile photo URL"profilePicture: String!"The host's profile bio description, will be shown in the listing"profileDescription: String!}Let's start with the
name
field and apply the@override
directive. We'll include thefrom
argument, which we'll set to themonolith
subgraph.name: String! @override(from: "monolith")We'll also need to add the directive to the import at the top.
extend schema@link(url: "https://specs.apollo.dev/federation/v2.7"import: ["@key", "@shareable", "@inaccessible", "@override"])When we save our changes,
rover dev
will automatically detect the change and trigger a composition... uh-oh! We've got 2 build errors:error[E029]: Encountered 2 build errors while trying to build a supergraph.Caused by:INVALID_FIELD_SHARING: Non-shareable field "Host.profilePicture" is resolved from multiple subgraphs: it is resolved from subgraphs "accounts" and "monolith" and defined as non-shareable in all of themINVALID_FIELD_SHARING: Non-shareable field "Host.profileDescription" is resolved from multiple subgraphs: it is resolved from subgraphs "accounts" and "monolith" and defined as non-shareable in all of themThe subgraph schemas you provided are incompatible with each other. See https://www.apollographql.com/docs/federation/errors/ for more information on resolving build errors.The
INVALID_FIELD_SHARING
error points to two fields:Host.profilePicture
andHost.profileDescription
. Reading the description explains the source of the error: we're defining a field twice, in separate subgraphs, and it's not a@shareable
field. Marking it@shareable
in both subgraphs would fix the error, but we're not interested in making it shareable! We know that these fields belong to theaccounts
subgraph, not themonolith
subgraph; that's why we're migrating after all.Let's finish up applying the
@override
directive to both fields.profilePicture: String! @override(from: "monolith")profileDescription: String! @override(from: "monolith")When we save our changes and
rover dev
re-composes, we'll see a successful composition with no errors. Awesome, that's it for theHost
entity!
Checking our changes
Before we keep going, let's head over to the local rover dev
router running at http://localhost:4000.
Let's build a query to get a specific host's
overallRating
.query User($userId: ID!) {user(id: $userId) {... on Host {overallRating}}}We'll set the
userId
variable touser-1
, the ID of a host user.{"userId": "user-1"}We can run the query and get a response back, but let's take a peek at the query plan. Remember, the query plan is the set of steps the router takes to resolve a GraphQL operation. Click the arrow beside "Response" and select Query Plan. We'll look at it as a chart.
http://localhost:4000A host's
overallRating
field belongs in themonolith
subgraph, and we see from the query plan that the router is fetching from that subgraph only.Let's add the
name
field to the operation now, a field we recently migrated over toaccounts
.query User($userId: ID!) {user(id: $userId) {... on Host {overallRatingname}}}Running the query again, we get a new plan! This time, we have an additional fetch to the
accounts
subgraph. This means our migration is working! 🎉http://localhost:4000We can also view the query plan as text to validate that the
name
field is coming from theaccounts
subgraph.http://localhost:4000
Awesome, let's keep going!
✏️ The Guest
entity
Let's use the same process for the Guest
entity. In the monolith/schema.graphql
file, we'll look at the Guest
type and label each field with which subgraph should be responsible for it.
"A guest is a type of Airlock user. They book places to stay."type Guest implements User {id: ID! # the key field, a unique identifier for a particular instance of a Guestname: String! # accounts subgraphprofilePicture: String! # accounts subgraphfunds: Float! # payments subgraph, eventually}
Using the plan above, we'll need to do the following:
- Migrate two fields (
name
andprofilePicture
) over to theaccounts
subgraph, which will be responsible for contributing those fields to theGuest
entity. - The
monolith
subgraph will keep contributing thefunds
field (eventually, thepayments
subgraph will, but let's take it one step at a time!).
Go ahead and make those changes. By the end of those steps, our accounts
schema for the Guest
entity should look like this:
"A guest is a type of Airlock user. They book places to stay."type Guest implements User @key(fields: "id") {id: ID!"The user's first and last name"name: String! @override(from: "monolith")"The user's profile photo URL"profilePicture: String! @override(from: "monolith")}
The rest of the types
Let's review the list again:
- ✅
User
interface withid
,name
,profilePicture
- ✅
Host
type withid
,name
,profilePicture
andprofileDescription
- ✅
Guest
type withid
,name
,profilePicture
Query.user
Query.me
Mutation.updateProfile
- The types used by the
updateProfile
mutation:UpdateProfileInput
,UpdateProfileResponse
andMutationResponse
Now it's your turn to implement the rest of the list. You've got this! Compare your code with ours after you're done.
type Query {user(id: ID!): User @override(from: "monolith")"Currently logged-in user"me: User! @override(from: "monolith")}type Mutation {"Updates the logged-in user's profile information"updateProfile(updateProfileInput: UpdateProfileInput): UpdateProfileResponse!@override(from: "monolith")}interface MutationResponse {"Similar to HTTP status code, represents the status of the mutation"code: Int!"Indicates whether the mutation was successful"success: Boolean!"Human-readable message for the UI"message: String!}"Fields that can be updated"input UpdateProfileInput {"The user's first and last name"name: String"The user's profile photo URL"profilePicture: String"The host's profile bio description, will be shown in the listing"profileDescription: String}"The response after updating a profile"type UpdateProfileResponse implements MutationResponse {"Similar to HTTP status code, represents the status of the mutation"code: Int! @override(from: "monolith")"Indicates whether the mutation was successful"success: Boolean! @override(from: "monolith")"Human-readable message for the UI"message: String! @override(from: "monolith")"Updated user"user: User @override(from: "monolith")}
Note: You might have noticed that we didn't need to apply @override
to the fields on UpdateProfileInput
. This is because an input type is an example of a value type that can be shared across subgraphs automatically. You can find out more about input types in federated schemas in the Apollo documentation.
Don't forget to clean up the Query
type: we don't need that stub _todo
field anymore.
- _todo: String @shareable @inaccessible
Let's save our changes and make sure there are no error messages in the rover dev
process; this ensures that composition was successful!
Implementing progressive override
By using the @override
directive, we flipped the switch for the Host
and Guest
fields to be resolved by the accounts
subgraph. All GraphQL operations requesting these fields will be taken care of by the accounts
subgraph.
In tutorial land, we're sticking with the basic override, but feel free to expand the section below for instructions on how to implement progressive override.
Practice
@override
directive?Key takeaways
- The
@override
directive can be applied to fields of an entity and fields of root operation types to indicate which subgraph should have the responsibility of resolving it. - The
@override
directive accepts an argument calledfrom
, which specifies the name of the subgraph that originally defined the field (and which is being overridden). - With progressive override, we can provide the
@override
directive with an additional argument,label
. This specifies the percentage of time the router should call upon the subgraph to resolve the field.
Up next
Our schema looks great, but we don't have any resolvers to actually make it work! Let's tackle that next.
Share your questions and comments about this lesson
Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.