Using the @provides Directive

Optimize query performance by resolving fields from specific query paths


The @provides directive optimizes query performance by reducing the number of subgraph calls needed to resolve queries.

This directive creates a relationship between two subgraphs:

  • The source of truth subgraph owns the field and can always resolve it.

  • The providing subgraph uses @provides to return the same data more efficiently at specific query paths.

Both subgraphs must resolve the field identically to ensure data consistency. It must not change the logic or behavior of your queries.

When to use @provides

Use @provides when all of the following conditions are met:

  • The query works without @provides. The directive only improves performance; it never changes the logical behavior of your GraphQL operations.

  • The data is identical. Both subgraphs must resolve the field identically to ensure data consistency.

  • You can maintain data consistency between the source of truth and the providing subgraph.

  • You have a specific performance goal to reduce the number of subgraph calls.

  • The optimization is specific to a query path. The optimization must be scoped to a specific query path instead of any entry point that can reach the provided fields.

  • You can ensure privacy and security compliance. Storing and accessing the provided data in the providing subgraph's storage system must comply with your organization's privacy and security requirements.

Identifying common use cases

You can use @provides for the following scenarios:

  • Search optimization: A search subgraph (for example, backed by Elasticsearch) returns fields owned by another subgraph to avoid additional round trips. For example, a search index might store product names and prices alongside search metadata, allowing it to return product information without calling the Products subgraph.

  • Read-only computed fields: Fields that derive their value from other fields and have no side effects, where the providing subgraph can compute them identically. For example, a search subgraph might compute a relevance score or ranking that matches what the source subgraph calculates.

  • Optimized data stores: Specialized stores (Elasticsearch, Redis, and others) that contain a subset of data from the source of truth reduce subgraph calls for specific query paths. The specialized store returns frequently accessed fields directly, avoiding a round trip to the source.

  • Performance-critical queries: Frequently executed queries benefit from reducing the number of subgraph calls. If a query path is called thousands of times per second, eliminating even one subgraph call significantly reduces latency and load.

Ensuring data consistency

Composition rules ensure your schema is valid, but they don't guarantee that your data is consistent. You're responsible for maintaining data consistency.

  • Data retention: The providing subgraph and the source of truth subgraph might have different data retention policies. This is acceptable as long as the providing subgraph has all the data needed for the queries that use @provides. Document the query's scope clearly.

  • Eventual consistency: Account for eventual consistency—the time delay between when data is updated in the source of truth and when it synchronizes to the providing subgraph. Use @provides only when eventual consistency is acceptable for your use case.

When to avoid using @provides

Avoid using @provides in these scenarios:

  • Fixing slow subgraph performance: @provides is a query planning optimization, not a fix for slow queries or database bottlenecks. Bypassing a slow subgraph with @provides hides the problem and adds complexity without addressing the root cause. Optimize at the subgraph level instead by using database tuning, caching, or query optimization.

  • Introducing logical differences: The provided data must match the source data exactly. Do not use @provides to introduce logical differences or alternate data sources.

  • Possible data divergence: If there's any possibility of data divergence, don't use @provides. This includes scenarios where data retention policies differ significantly and cannot be mitigated through query scope, eventual consistency delays are unacceptable for your use case, or different subgraphs might resolve the field differently.

  • Hiding data inconsistencies: Never use @provides to hide conflicting data between subgraphs. Fix the data at the source.

  • Sensitive data without compliance: Don't use @provides for sensitive data (PII, financial information, and other sensitive fields) unless the providing subgraph's storage system is approved and compliant with all relevant regulations.

Implementing @provides in your schema

Implement @provides in your schema:

  1. Test your query to confirm it works without @provides.

  2. Verify that data is identical across subgraphs to ensure consistency.

  3. Mark the field as @shareable in the source-of-truth subgraph.

GraphQL
Products subgraph (source of truth)
1type Product @key(fields: "id") {
2  id: ID!
3  price: Float! @shareable  # Step 3: mark the field as @shareable
4}
  1. Mark the field as @external in the providing subgraph.

  2. Add @provides to the query path that returns the provided field.

GraphQL
Search subgraph (providing subgraph)
1type Product @key(fields: "id") {
2  id: ID!
3  price: Float! @external # Step 4: mark the field as @external
4}
5
6type SearchResult {
7  products: [Product!]! @provides(fields: "price") # Step 5: add @provides to the query path that returns the provided field
8}
  1. Monitor performance to verify that the optimization works.

Composition rules

Apollo Federation enforces these composition rules. If a subgraph provides an entity field using @provides:

  • The subgraph must define that field and mark it @external.

  • The entity field must be marked as either @shareable or @external in every subgraph that defines it.

  • The entity field must be marked as @shareable in at least one other subgraph so that at least one subgraph can always resolve the field.

Composition fails if you violate these rules.

Verifying your optimization

When the router encounters a query with a field that uses @provides, it determines if it can satisfy the entire query from the providing subgraph. If it can, it makes a single subgraph call and skips the source of truth—that's the optimization. If it can't, it falls back to calling both subgraphs and there is no optimization.

To verify that the optimization is working, compare the before and after for the operation's query plan and metrics in GraphOS Studio.

Example: No optimization

In this example, the providing subgraph (the Search subgraph) only provides the price field.

GraphQL
Products subgraph (source of truth)
1type Product @key(fields: "id") {
2  id: ID!
3  name: String!
4  price: Float! @shareable
5  description: String!
6}
GraphQL
Search subgraph (providing subgraph)
1type Product @key(fields: "id") {
2  id: ID!
3  price: Float! @external
4}
5
6type SearchResult {
7  products: [Product!]! @provides(fields: "price")  # Only provides price
8}
9
10type Query {
11  searchProducts(query: String!): SearchResult
12}

However, the query requests multiple fields for a product: id, name, price, and description. The router must call the Products subgraph for the rest—no optimization.

GraphQL
Query
1query {
2  searchProducts(query: "laptop") { # Router calls Search subgraph
3    products {
4      id
5      price      # @provides only covers this field
6      name
7      description # Router still needs to call Products subgraph for id, name, description fields
8    }
9  }
10}

The query plan contains 2 subgraph calls:

  • Call 1: Search subgraph (list of products and their price)

  • Call 2: Products subgraph (id, name, description)

This results in a failed optimization with no benefits.

Example: Successful optimization

In this example, the providing subgraph (the Search subgraph) provides the name and price fields.

GraphQL
Products subgraph (source of truth)
1type Product @key(fields: "id") {
2  id: ID!
3  name: String! @shareable
4  price: Float! @shareable
5}
GraphQL
Search subgraph (providing subgraph)
1type Product @key(fields: "id") {
2  id: ID!
3  name: String! @external
4  price: Float! @external
5}
6
7type SearchResult {
8  products: [Product!]! @provides(fields: "name price")  # Provides all needed fields
9}
10
11type Query {
12  searchProducts(query: String!): SearchResult
13}
GraphQL
Query
1query {
2  searchProducts(query: "laptop") { # Router calls Search subgraph
3    products {
4      id         # All fields are provided, no need to call the Products subgraph
5      name
6      price
7    }
8  }
9}

The query plan contains a single subgraph call to the Search subgraph to return the id, name, and price fields. This is a successful optimization.

Examples for using @provides

Data retention with documented scope

In this example, the Products subgraph (source of truth) has all historical data, but the Search subgraph (providing subgraph) only has data from the last 30 days.

GraphQL
Products subgraph (source of truth - all historical data)
1type Product @key(fields: "id") {
2  id: ID!
3  name: String! @shareable
4  price: Float! @shareable
5}
GraphQL
Search subgraph (providing subgraph - 30 days retention)
1type Product @key(fields: "id", resolvable: false) {
2  id: ID!
3  name: String! @external
4  price: Float! @external
5}
6
7type Query {
8  """Searches for products added in the last 30 days"""
9  searchRecentProducts(query: String!): [Product!]! @provides(fields: "name price")
10}

The query is documented to only search products from the last 30 days, and the backing data source (for example, Elasticsearch) retains 30+ days of data. The query path itself limits the scope, making @provides valid even though overall retention differs.

Privacy-compliant field selection

In this example, the Users subgraph (source of truth) provides all user data, but the Search subgraph (providing subgraph) provides only non-sensitive fields.

GraphQL
Users subgraph (source of truth)
1type User @key(fields: "id") {
2  id: ID!
3  username: String! @shareable
4  displayName: String! @shareable
5  email: String!
6  phoneNumber: String!
7  ssn: String!
8}
GraphQL
Search subgraph (providing only non-sensitive fields)
1type User @key(fields: "id") {
2  id: ID!
3  username: String! @external
4  displayName: String! @external
5  # email, phoneNumber, ssn are NOT provided
6}
7
8type SearchResult {
9  users: [User!]! @provides(fields: "username displayName")
10}
11
12type Query {
13  searchUsers(query: String!): SearchResult
14}

Only non-sensitive fields are provided. Sensitive fields (email, phoneNumber, ssn) remain in the Users subgraph even though Elasticsearch might have access to them, ensuring compliance with privacy and security requirements.

Using @provides with interface implementations

In this example, Product is an interface type with two implementing types: PhysicalProduct and DigitalProduct. Both the Products subgraph (source of truth) and the Search subgraph (providing subgraph) can provide data for each implementing type.

GraphQL
Products subgraph (source of truth)
1interface Product @key(fields: "id") {
2  id: ID!
3  title: String!
4}
5
6type PhysicalProduct implements Product @key(fields: "id") {
7  id: ID!
8  title: String! @shareable
9  width: Float! @shareable
10  height: Float! @shareable
11  depth: Float! @shareable
12}
13
14type DigitalProduct implements Product @key(fields: "id") {
15  id: ID!
16  title: String! @shareable
17  downloadSize: Int! @shareable
18  fileFormat: String! @shareable
19  licenseType: String! @shareable
20}
GraphQL
Search subgraph (providing subgraph)
1
2type Query {
3  searchProducts: Product @provides(fields: """ 
4    ... on PhysicalProduct {
5      title
6      width
7      height
8      depth
9    }
10    ... on DigitalProduct {
11      title
12      downloadSize
13      fileFormat
14      licenseType
15    }
16  """)
17}
18
19interface Product @key(fields: "id", resolvable: false) {
20  id: ID!
21  title: String!
22}
23
24type PhysicalProduct implements Product @key(fields: "id", resolvable: false) {
25  id: ID!
26  title: String! @external
27  width: Float! @external
28  height: Float! @external
29  depth: Float! @external
30}
31
32type DigitalProduct implements Product @key(fields: "id", resolvable: false) {
33  id: ID!
34  title: String! @external
35  downloadSize: Int! @external
36  fileFormat: String! @external
37  licenseType: String! @external
38}

The searchProducts query provides fields for both PhysicalProduct and DigitalProduct using inline fragments to specify which fields to provide for each implementing type.

Using @provides with interface objects

GraphQL
Products subgraph (source of truth)
1interface Product @key(fields: "id") {
2  id: ID!
3  title: String!
4}
5
6type PhysicalProduct implements Product @key(fields: "id") {
7  id: ID!
8  title: String! @shareable
9  width: Float! @shareable
10  height: Float! @shareable
11  depth: Float! @shareable
12}
13
14type DigitalProduct implements Product @key(fields: "id") {
15  id: ID!
16  title: String! @shareable
17  downloadSize: Int! @shareable
18  fileFormat: String! @shareable
19  licenseType: String! @shareable
20}
GraphQL
Search subgraph (providing subgraph)
1type Query {
2  searchProducts: Product @provides(fields: "title")
3}
4
5type Product @key(fields: "id", resolvable: false) @interfaceObject {
6  id: ID!
7  title: String! @external
8}

The searchProducts query provides the title field for the Product interface object using @interfaceObject.

When using @provides on an @interfaceObject type, every implementation of that interface must mark the provided field as @shareable because @shareable and @external cannot be used on interfaces. In this example, both PhysicalProduct and DigitalProduct mark title as @shareable.

Using @provides with unions

In this example, Product is a union type with two member types: PhysicalProduct and DigitalProduct. Both the Products subgraph (source of truth) and the Search subgraph (providing subgraph) provide data for each member type.

GraphQL
Products subgraph (source of truth)
1type PhysicalProduct @key(fields: "id") {
2  id: ID!
3  title: String! @shareable
4  width: Float! @shareable
5  height: Float! @shareable
6  depth: Float! @shareable
7}
8
9type DigitalProduct @key(fields: "id") {
10  id: ID!
11  title: String! @shareable
12  downloadSize: Int! @shareable
13  fileFormat: String! @shareable
14  licenseType: String! @shareable
15}
16
17union Product = PhysicalProduct | DigitalProduct
GraphQL
Search subgraph (providing subgraph)
1type Query {
2  searchProducts: Product @provides(fields: """
3    ... on PhysicalProduct {
4      title
5      width
6      height
7      depth
8    }
9    ... on DigitalProduct {
10      title
11      downloadSize
12      fileFormat
13      licenseType
14    }
15  """)
16}
17
18type PhysicalProduct @key(fields: "id", resolvable: false) {
19  id: ID!
20  title: String! @external
21  width: Float! @external
22  height: Float! @external
23  depth: Float! @external
24}
25
26type DigitalProduct @key(fields: "id", resolvable: false) {
27  id: ID!
28  title: String! @external
29  downloadSize: Int! @external
30  fileFormat: String! @external
31  licenseType: String! @external
32}
33
34union Product = PhysicalProduct | DigitalProduct

The searchProducts query provides fields for union members using inline fragments to specify which fields to provide for each union member type.

Feedback

Ask Community