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
@providesto 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
@providesonly when eventual consistency is acceptable for your use case.
When to avoid using @provides
Avoid using @provides in these scenarios:
Fixing slow subgraph performance:
@providesis a query planning optimization, not a fix for slow queries or database bottlenecks. Bypassing a slow subgraph with@provideshides 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
@providesto 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
@providesto hide conflicting data between subgraphs. Fix the data at the source.Sensitive data without compliance: Don't use
@providesfor 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:
Test your query to confirm it works without
@provides.Verify that data is identical across subgraphs to ensure consistency.
Mark the field as
@shareablein the source-of-truth subgraph.
1type Product @key(fields: "id") {
2 id: ID!
3 price: Float! @shareable # Step 3: mark the field as @shareable
4}Mark the field as
@externalin the providing subgraph.Add
@providesto the query path that returns the provided field.
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}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
@shareableor@externalin every subgraph that defines it.The entity field must be marked as
@shareablein 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.
1type Product @key(fields: "id") {
2 id: ID!
3 name: String!
4 price: Float! @shareable
5 description: String!
6}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.
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.
1type Product @key(fields: "id") {
2 id: ID!
3 name: String! @shareable
4 price: Float! @shareable
5}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}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.
1type Product @key(fields: "id") {
2 id: ID!
3 name: String! @shareable
4 price: Float! @shareable
5}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.
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}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.
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}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
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}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.
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 | DigitalProduct1type 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 | DigitalProductThe searchProducts query provides fields for union members using inline fragments to specify which fields to provide for each union member type.