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
@overridedirective - Learn about the progressive
@overrideapproach for incremental migration - Start to migrate our
accountssubgraph 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!
Userinterface withid,name,profilePictureHosttype withid,name,profilePictureandprofileDescriptionGuesttype withid,name,profilePictureQuery.userQuery.meMutation.updateProfile- The types used by the
updateProfilemutation:UpdateProfileInput,UpdateProfileResponseandMutationResponse
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,profilePictureandprofileDescription) over to theaccountssubgraph, which will be responsible for contributing those fields to theHostentity. - The
monolithsubgraph will keep contributing theoverallRatingfield (eventually, thereviewssubgraph 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
monolithsubgraph schema, let's copy theHosttype, its key field and the three fields it'll be responsible for (name,profilePictureandprofileDescription) over to theaccountssubgraph 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
namefield and apply the@overridedirective. We'll include thefromargument, which we'll set to themonolithsubgraph.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 devwill 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_SHARINGerror points to two fields:Host.profilePictureandHost.profileDescription. Reading the description explains the source of the error: we're defining a field twice, in separate subgraphs, and it's not a@shareablefield. Marking it@shareablein both subgraphs would fix the error, but we're not interested in making it shareable! We know that these fields belong to theaccountssubgraph, not themonolithsubgraph; that's why we're migrating after all.Let's finish up applying the
@overridedirective to both fields.profilePicture: String! @override(from: "monolith")profileDescription: String! @override(from: "monolith")When we save our changes and
rover devre-composes, we'll see a successful composition with no errors. Awesome, that's it for theHostentity!
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
userIdvariable 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:4000
A host's
overallRatingfield belongs in themonolithsubgraph, and we see from the query plan that the router is fetching from that subgraph only.Let's add the
namefield 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
accountssubgraph. This means our migration is working! 🎉http://localhost:4000
We can also view the query plan as text to validate that the
namefield is coming from theaccountssubgraph.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 (
nameandprofilePicture) over to theaccountssubgraph, which will be responsible for contributing those fields to theGuestentity. - The
monolithsubgraph will keep contributing thefundsfield (eventually, thepaymentssubgraph 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:
- ✅
Userinterface withid,name,profilePicture - ✅
Hosttype withid,name,profilePictureandprofileDescription - ✅
Guesttype withid,name,profilePicture Query.userQuery.meMutation.updateProfile- The types used by the
updateProfilemutation:UpdateProfileInput,UpdateProfileResponseandMutationResponse
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
@overridedirective 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
@overridedirective 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
@overridedirective 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.