Progressive Override
As a supergraph evolves, you often need to move fields from one subgraph to another. For example,
imagine you are migrating the status of an Order from a general orders subgraph to a new, more
specialized fulfillment subgraph.
How Progressive Override Works
Progressive override works in combination with Apollo Federation’s @override directive. When you
use @override with a label, you create a controlled migration path that can be activated
selectively based on request criteria.
Here’s the basic flow:
- The field exists in both the old subgraph and the new subgraph
- The new subgraph’s field has an
@overridedirective with a label (e.g.,"use-fulfillment-service") - The gateway’s
progressiveOverridefunction determines if the label is “active” for each request - If active → use the new subgraph’s field
- If inactive → continue using the old subgraph’s field
This allows you to gradually migrate traffic while maintaining reliability.
Feature Flag Approach
The most common approach is to use a feature flag mechanism to control when the override is active:
When a label like "use-fulfillment-service" is “active” for a request, the gateway will resolve
Order.status from the new fulfillment subgraph. When it’s inactive, it will continue to use the
original orders subgraph.
The progressiveOverride configuration in the gateway is the mechanism that determines which labels
are active for any given request.
progressiveOverride function is called per used label and request, allowing you to implement any
logic to determine if the label should be active or not either based on percentages, headers, user
IDs, or any other criteria.
Important Performance Considerations
⚠️ This function runs on every request, so it must be highly performant:
- Avoid network calls at all cost
- Cache expensive computations (e.g., feature flag evaluations)
- Use simple hash functions for user-based rollouts for consistency
- Return
falsequickly for labels you don’t recognize or raise errors
Best Practices
- Always return a boolean - Don’t return
undefinedornull - Make it deterministic - Same request → same result (important for caching and consistency)
- Default to
false- Safer to keep old behavior unless explicitly enabled - Test thoroughly - Verify both active and inactive paths work correctly
Example: LaunchDarkly Integration
You can see a working example with LaunchDarkly that holds the flag state externally. The gateway then decides whether to activate the override based on the flag value dynamically per request.
See the LaunchDarkly example here.
Percentage Approach
For simple percentage-based rollouts, you can use the built-in percent(x) label syntax without
writing custom logic.
How percent(x) Works
- Syntax:
label: "percent(N)"where N is a number between 0 and 100 - No gateway config needed: Works automatically without
progressiveOverridefunction - Stable per operation: Same query → same percentage bucket
Percentage vs Labels
| Approach | Use When | Pros | Cons |
|---|---|---|---|
Percentage percent(x) | Simple gradual rollouts | No config needed, consistent | Limited to percentages |
| Labels | Complex criteria (headers, user traits, etc.) | Full control, feature flags | Requires implementation |
Async Support
The progressiveOverride function can be async and return a Promise. This enables advanced use
cases like fetching from external services or database queries.
When to Use Async
Use async mode when you need to:
- Fetch from external feature flag services (LaunchDarkly, Split, etc.)
- Query a database for user-specific flags
- Perform complex calculations that require I/O
⚠️ Performance Warning: Async calls add latency to every request. Consider caching strategies to minimize external calls.
Example: External Feature Flag Service
Async Best Practices
- Set timeouts - Prevent slow services from blocking requests
- Fail safely - Default to
falsewhen unsure (keeps old behavior) - Cache results - Avoid repeated calls for the same data
- Monitor latency - Track async call duration in metrics
Example: With Caching
Complete Migration Walkthrough
Here’s a step-by-step example of migrating Order.status from orders subgraph to fulfillment
subgraph:
Step 1: Add Field to New Subgraph
Step 2: Configure Gateway
Step 3: Gradual Rollout
- Deploy the new subgraph and gateway config
- Monitor - Everything still uses old subgraph (0%)
- Test internally - Set env var to
truefor internal testing - 10% rollout - Set
FULFILLMENT_ROLLOUT_PERCENT=10 - Monitor metrics - Check error rates, latency, correctness
- Increase gradually - 25%, 50%, 75%, 100%
- Remove old field - Once at 100%, remove from old subgraph
Step 4: Cleanup
After successful migration:
Common Gotchas
1. Label Naming
Bad:
Good:
2. Caching Issues
If you see inconsistent behavior, your GraphQL operation cache might be affecting results. Progressive override decisions are made per-request, but GraphQL operation caching happens separately.
3. Missing Fields in Old Subgraph
Make sure the old subgraph still has the field during migration. The override only tells the gateway where to get it from - both need to exist until migration is complete.
4. Schema Composition Order
When updating multiple subgraphs, deploy the new subgraph before updating the old one to avoid composition errors.
5. Testing Edge Cases
Test these scenarios:
- Field throws an error in new subgraph
- New subgraph is slow or unavailable
- Network timeouts
- User doesn’t have feature flag access
Monitoring & Debugging
Add logging to track override activation:
In production, use structured logging with request IDs to trace which path was taken.
Summary
- Progressive override enables safe, gradual field migrations
- Use
percent(x)for simple percentage rollouts - Use custom logic for feature flags and complex criteria
- Async mode enables external service integration
- Always test thoroughly and monitor metrics during rollout
- Default to
falsefor safe behavior - Clean up old fields after successful migration