9. Using @override to migrate fields
7m

Overview

Let's continue building our accounts ! We have our list of types and that we need to migrate from the monolith . But how do we do this safely?

In this lesson, we will:

  • Learn about the @override
  • Learn about the progressive @override approach for incremental migration
  • Start to migrate our accounts types

The @override directive

To migrate safely from one to another, we use the @override .

We can apply @override to of an and fields of root types (such as Query and Mutation). It tells the that a particular is now resolved by the that applies @override, instead of another where the is also defined.

The @override takes in an called from, which will be the name of the that originally defined the .

Let's take the Query.user as an example, which currently lives in the monolith . We want to move it over to the accounts . In the accounts schema, we would add the just after the return type of the :

subgraph-accounts/schema.graphql
type Query {
user(id: ID!): User @override(from: "monolith")
}

The monolith can stay the same, but the won't call on it anymore when the user 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 towards the accounts . 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 to the @override : label. This takes in a string value starting with percent followed by a number in parentheses. The number represents the percentage of traffic for the that's resolved by this . The remaining percentage is resolved by the other (from) .

Let's examine the Query.user again. If we wanted the accounts to handle only 25% of traffic, we would annotate it like so:

schema.graphql
type Query {
user(id: ID!): User @override(from: "monolith", label: "percent(25)")
}

The monolith still stays the same, but now the will route its requests to the monolith 75% of the time, and the accounts 25% of the time for the Query.user .

Let's migrate!

Here's our list of types and for the accounts 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 with id, name, profilePicture
  • Host type with id, name, profilePicture and profileDescription
  • Guest type with id, name, profilePicture
  • Query.user
  • Query.me
  • Mutation.updateProfile
  • The types used by the updateProfile : UpdateProfileInput, UpdateProfileResponse and MutationResponse

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-accounts/schema.graphql
"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 ? Well, we still need to keep the User interface defined there. We reference it as a return type for the Review.author , and we also implement the interface for the Host and Guest types.

However, we don't need to keep all the extra that are the responsibility of the accounts ! We can remove both name and profilePicture.

monolith/schema.graphql
"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 , 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 by default, and those definitions can differ in each subgraph. This is how we can remove the name and profilePicture from the User interface definition in the monolith !

Now that we've got our User interface squared away in both , 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 . For each field, we've added a comment indicating which should be responsible for that field, based on the separation of concerns principle.

monolith/schema.graphql
type Host implements User @key(fields: "id") {
id: ID! # the key field, a unique identifier for a particular instance of a Host
name: String! # accounts subgraph
profilePicture: String! # accounts subgraph
profileDescription: String! # accounts subgraph
overallRating: Float # reviews subgraph, eventually
}

Using the plan above, we'll need to do the following:

  • Migrate three (name, profilePicture and profileDescription) over to the accounts , which will be responsible for contributing those to the Host .
  • The monolith will keep contributing the overallRating (eventually, the reviews 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 from the monolith (on the left), and duplicating them over to the accounts (on the right).

  1. From the monolith , let's copy the Host type, its key and the three fields it'll be responsible for (name, profilePicture and profileDescription) over to the accounts .

    subgraph-accounts/schema.graphql
    type 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!
    }
  2. Let's start with the name and apply the @override . We'll include the from , which we'll set to the monolith .

    name: String! @override(from: "monolith")
  3. We'll also need to add the to the import at the top.

    extend schema
    @link(
    url: "https://specs.apollo.dev/federation/v2.7"
    import: ["@key", "@shareable", "@inaccessible", "@override"]
    )
  4. When we save our changes, rover dev will automatically detect the change and trigger a ... 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 them
    INVALID_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 them
    The 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 : Host.profilePicture and Host.profileDescription. Reading the description explains the source of the error: we're defining a twice, in separate , and it's not a @shareable . Marking it @shareable in both would fix the error, but we're not interested in making it shareable! We know that these belong to the accounts , not the monolith ; that's why we're migrating after all.

  5. Let's finish up applying the @override to both .

    profilePicture: String! @override(from: "monolith")
    profileDescription: String! @override(from: "monolith")
  6. When we save our changes and rover dev re-composes, we'll see a successful with no errors. Awesome, that's it for the Host !

Checking our changes

Before we keep going, let's head over to the local rover dev running at http://localhost:4000.

  1. Let's build a to get a specific host's overallRating.

    query User($userId: ID!) {
    user(id: $userId) {
    ... on Host {
    overallRating
    }
    }
    }

    We'll set the userId to user-1, the ID of a host user.

    {
    "userId": "user-1"
    }
  2. We can run the and get a response back, but let's take a peek at the . Remember, the query plan is the set of steps the takes to resolve a . Click the arrow beside "Response" and select Query Plan. We'll look at it as a chart.

    http://localhost:4000

    Query plan showing only the monolith subgraph

    A host's overallRating belongs in the monolith , and we see from the that the is fetching from that subgraph only.

  3. Let's add the name to the now, a field we recently migrated over to accounts.

    query User($userId: ID!) {
    user(id: $userId) {
    ... on Host {
    overallRating
    name
    }
    }
    }
  4. Running the 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:4000

    Query plan showing both subgraphs

  5. We can also view the as text to validate that the name is coming from the accounts .

    http://localhost:4000

    Query plan showing both subgraphs

Awesome, let's keep going!

✏️ The Guest entity

Let's use the same process for the Guest . In the monolith/schema.graphql file, we'll look at the Guest type and label each with which should be responsible for it.

monolith/schema.graphql
"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 Guest
name: String! # accounts subgraph
profilePicture: String! # accounts subgraph
funds: Float! # payments subgraph, eventually
}

Using the plan above, we'll need to do the following:

  • Migrate two (name and profilePicture) over to the accounts , which will be responsible for contributing those to the Guest .
  • The monolith will keep contributing the funds (eventually, the payments 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 should look like this:

subgraph-accounts/schema.graphql
"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 with id, name, profilePicture
  • Host type with id, name, profilePicture and profileDescription
  • Guest type with id, name, profilePicture
  • Query.user
  • Query.me
  • Mutation.updateProfile
  • The types used by the updateProfile : UpdateProfileInput, UpdateProfileResponse and MutationResponse

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.

subgraph-accounts/schema.graphql
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 on UpdateProfileInput. This is because an input type is an example of a value type that can be shared across 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 anymore.

subgraph-accounts/schema.graphql
- _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 was successful!

Implementing progressive override

By using the @override , we flipped the switch for the Host and Guest to be resolved by the accounts . All requesting these will be taken care of by the accounts .

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

Which of the following is true about the @override directive?

Key takeaways

  • The @override can be applied to of an and fields of root types to indicate which should have the responsibility of resolving it.
  • The @override accepts an called from, which specifies the name of the that originally defined the (and which is being overridden).
  • With progressive override, we can provide the @override with an additional , label. This specifies the percentage of time the should call upon the to resolve the .

Up next

Our schema looks great, but we don't have any to actually make it work! Let's tackle that next.

Previous

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.