Repository: apollographql/router
Pull request #6888: fix(entity_caching): do not include other fields from representation variable than the entity keys
Author: login: bnjjj
Labels: name: backport-1.x
description: Backport this PR to 1.x
Created: 2025-03-03T11:03:48Z | Merged: 2025-03-10T11:08:50Z

Separate entity keys and representation variable value in the cache key to avoid issues with `@requires` for example.

close #6673 

<!-- [ROUTER-1007] --> 
<!-- [ROUTER-1008] --> 

[ROUTER-1007]: https://apollographql.atlassian.net/browse/ROUTER-1007?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[ROUTER-1008]: https://apollographql.atlassian.net/browse/ROUTER-1008?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Linked issue #6673: Entity caching uses stringified representations within entity cache key
## Describe the bug

Enttiy caching works by calculating a cache key that can be repeated for easier invalidation. Unfortunately the code: https://github.com/apollographql/router/blob/7a015fdadf416495f961fe621569580b8cf1181a/apollo-router/src/plugins/cache/entity.rs#L1337

Uses the entity _representation_, meaning that if a type uses `@requires`, it'd be included as part of the cache key. 

Furthermore, this representation is hashed as provided by either the router or user, meaning that the hash key is dependent on the order of a representation's values. For example:

```json
{
    "id": "1234",
    "id2": "5678"
}
```

is not the same as:

```json
{
  "id2": "5678",
  "id": "1234"
}
```

For purposes of hash calculation. 

This poses two problems: 

- Invalidation is unable to be easily used, as `@requires` are dynamically required as part of the request (and can be quite large payloads), meaning that users have to include all required inputs beyond keys, as well as matching the original order in which the router sent them
- The cache key includes data that isn't related to the actual key of the entity, so invalidating a single entity type isn't possible easily


## To Reproduce

For literal representation in the invalidation API: 

1. Configure the router and entity caching (with invalidation)with a Redis server
2. Use a `monitor` command to monitor the Redis commands issued by the router
3. Run a invalidation request, ordering the keys in different ways. You'll note the hash value in the SCAN command is different based on ordering

For literal representations within the cache:

1. Configure the router and entity caching (with invalidation)with a Redis server
2. Use a `monitor` command to monitor the Redis commands issued by the router
3. Have a schema that includes a type that has a `@requires` statement, and issue a request for that entity that includes the field that has `@requires` applied
5. issue a request for that entity that includes the field that has `@requires` applied
6. Issue a second request _without_ the field that has the `@requires` applied
7. Note that the hash prefixes differ


## Expected behavior

Keys are calculated without any required field data as well as ignoring ordering in invalidation requests. 

Linked issue comments: user: login: bnjjj
created_at: 2025-01-29T14:27:07Z
body: For the ordering issue: we should re-order the representations ourself before putting it in redis, and same when looking for this key in invalidation api.

For representations then we should remove the fields related to `@requires`. @lleadbet Could we have fields both requested by `@requires` and the entity itself ?
author_association: CONTRIBUTOR

Diff:
diff --git a/.changesets/fix_bnjjj_fix_entity_caching_entity_key.md b/.changesets/fix_bnjjj_fix_entity_caching_entity_key.md
new file mode 100644
index 0000000000..60c5caf889
--- /dev/null
+++ b/.changesets/fix_bnjjj_fix_entity_caching_entity_key.md
@@ -0,0 +1,11 @@
+### Do not include other fields from representation variable than the entity keys for entity cache([Issue #6673](https://github.com/apollographql/router/issues/6673))
+
+Separate entity keys and representation variable value in the cache key to avoid issues with `@requires` for example.
+
+close #6673
+
+> [!IMPORTANT]
+>
+> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release contains changes which necessarily alter the hashing algorithm used for the cache keys.  On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service.
+
+By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/6888
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 4cecac0607..c0d5ab35ac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,7 +47,7 @@ debug = 1
 # Dependencies used in more than one place are specified here in order to keep versions in sync:
 # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table
 [workspace.dependencies]
-apollo-compiler = "1.26.0"
+apollo-compiler = "1.27.0"
 apollo-parser = "0.8.4"
 apollo-smith = "0.15.0"
 async-trait = "0.1.77"
diff --git a/apollo-router/src/plugin/test/mock/subgraph.rs b/apollo-router/src/plugin/test/mock/subgraph.rs
index 50e3cacd71..0a96c941c1 100644
--- a/apollo-router/src/plugin/test/mock/subgraph.rs
+++ b/apollo-router/src/plugin/test/mock/subgraph.rs
@@ -199,7 +199,6 @@ impl Service<SubgraphRequest> for MockSubgraph {
         }
 
         normalize(body);
-
         let response = if let Some(response) = self.mocks.get(body) {
             // Build an http Response
             let mut http_response_builder = http::Response::builder().status(StatusCode::OK);
diff --git a/apollo-router/src/plugins/cache/entity.rs b/apollo-router/src/plugins/cache/entity.rs
index 670178b5de..7c8514cd39 100644
--- a/apollo-router/src/plugins/cache/entity.rs
+++ b/apollo-router/src/plugins/cache/entity.rs
@@ -5,8 +5,14 @@ use std::ops::ControlFlow;
 use std::sync::Arc;
 use std::time::Duration;
 
+use apollo_compiler::Name;
+use apollo_compiler::Schema;
+use apollo_compiler::ast::NamedType;
+use apollo_compiler::parser::Parser;
+use apollo_compiler::validation::Valid;
 use http::header;
 use http::header::CACHE_CONTROL;
+use indexmap::IndexMap;
 use multimap::MultiMap;
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -80,6 +86,9 @@ pub(crate) struct EntityCache {
     expose_keys_in_context: bool,
     private_queries: Arc<RwLock<HashSet<String>>>,
     pub(crate) invalidation: Invalidation,
+    supergraph_schema: Arc<Valid<Schema>>,
+    /// map containing the enum GRAPH
+    subgraph_enums: Arc<HashMap<String, String>>,
 }
 
 pub(crate) struct Storage {
@@ -312,6 +321,8 @@ impl Plugin for EntityCache {
             metrics: init.config.metrics,
             private_queries: Arc::new(RwLock::new(HashSet::new())),
             invalidation,
+            subgraph_enums: Arc::new(get_subgraph_enums(&init.supergraph_schema)),
+            supergraph_schema: init.supergraph_schema,
         })
     }
 
@@ -404,6 +415,8 @@ impl Plugin for EntityCache {
                     private_id,
                     invalidation: self.invalidation.clone(),
                     expose_keys_in_context: self.expose_keys_in_context,
+                    supergraph_schema: self.supergraph_schema.clone(),
+                    subgraph_enums: self.subgraph_enums.clone(),
                 });
             tower::util::BoxService::new(inner)
         } else {
@@ -465,6 +478,7 @@ impl EntityCache {
     pub(crate) async fn with_mocks(
         storage: RedisCacheStorage,
         subgraphs: HashMap<String, Subgraph>,
+        supergraph_schema: Arc<Valid<Schema>>,
     ) -> Result<Self, BoxError>
     where
         Self: Sized,
@@ -500,10 +514,33 @@ impl EntityCache {
                 concurrent_requests: 10,
             })),
             invalidation,
+            subgraph_enums: Arc::new(get_subgraph_enums(&supergraph_schema)),
+            supergraph_schema,
         })
     }
 }
 
+/// Get the map of subgraph enum variant mapped with subgraph name
+fn get_subgraph_enums(supergraph_schema: &Valid<Schema>) -> HashMap<String, String> {
+    let mut subgraph_enums = HashMap::new();
+    if let Some(graph_enum) = supergraph_schema.get_enum("join__Graph") {
+        subgraph_enums.extend(graph_enum.values.iter().filter_map(
+            |(enum_name, enum_value_def)| {
+                let subgraph_name = enum_value_def
+                    .directives
+                    .get("join__graph")?
+                    .specified_argument_by_name("name")?
+                    .as_str()?
+                    .to_string();
+
+                Some((enum_name.to_string(), subgraph_name))
+            },
+        ));
+    }
+
+    subgraph_enums
+}
+
 #[derive(Clone)]
 struct CacheService {
     service: subgraph::BoxCloneService,
@@ -515,6 +552,8 @@ struct CacheService {
     private_id: Option<String>,
     expose_keys_in_context: bool,
     invalidation: Invalidation,
+    supergraph_schema: Arc<Valid<Schema>>,
+    subgraph_enums: Arc<HashMap<String, String>>,
 }
 
 impl Service<subgraph::Request> for CacheService {
@@ -680,6 +719,8 @@ impl CacheService {
             let request_id = request.id.clone();
             match cache_lookup_entities(
                 self.name.clone(),
+                self.supergraph_schema.clone(),
+                &self.subgraph_enums,
                 self.storage.clone(),
                 is_known_private,
                 private_id.as_deref(),
@@ -926,8 +967,11 @@ async fn cache_lookup_root(
 
 struct EntityCacheResults(Vec<IntermediateResult>, Option<CacheControl>);
 
+#[allow(clippy::too_many_arguments)]
 async fn cache_lookup_entities(
     name: String,
+    supergraph_schema: Arc<Valid<Schema>>,
+    subgraph_enums: &HashMap<String, String>,
     cache: RedisCacheStorage,
     is_known_private: bool,
     private_id: Option<&str>,
@@ -935,9 +979,10 @@ async fn cache_lookup_entities(
     expose_keys_in_context: bool,
 ) -> Result<ControlFlow<subgraph::Response, (subgraph::Request, EntityCacheResults)>, BoxError> {
     let body = request.subgraph_request.body_mut();
-
     let keys = extract_cache_keys(
         &name,
+        supergraph_schema,
+        subgraph_enums,
         &request.query_hash,
         body,
         &request.context,
@@ -1306,8 +1351,11 @@ fn extract_cache_key_root(
 }
 
 // build a list of keys to get from the cache in one query
+#[allow(clippy::too_many_arguments)]
 fn extract_cache_keys(
     subgraph_name: &str,
+    supergraph_schema: Arc<Valid<Schema>>,
+    subgraph_enums: &HashMap<String, String>,
     query_hash: &QueryHash,
     body: &mut graphql::Request,
     context: &Context,
@@ -1326,18 +1374,52 @@ fn extract_cache_keys(
         .and_then(|value| value.as_array_mut())
         .expect("we already checked that representations exist");
 
+    // Get entity key to only get the right fields in representations
+
     let mut res = Vec::new();
     for representation in representations {
-        let opt_type = representation
-            .as_object_mut()
-            .and_then(|o| o.remove(TYPENAME))
+        let representation =
+            representation
+                .as_object_mut()
+                .ok_or_else(|| FetchError::MalformedRequest {
+                    reason: "representation variable should be an array of object".to_string(),
+                })?;
+        let typename_value =
+            representation
+                .remove(TYPENAME)
+                .ok_or_else(|| FetchError::MalformedRequest {
+                    reason: "missing __typename in representation".to_string(),
+                })?;
+
+        let typename = typename_value
+            .as_str()
             .ok_or_else(|| FetchError::MalformedRequest {
-                reason: "missing __typename in representation".to_string(),
+                reason: "__typename in representation is not a string".to_string(),
             })?;
+        let entity_keys = get_entity_keys_from_supergraph_schema(
+            typename,
+            subgraph_name,
+            &supergraph_schema,
+            subgraph_enums,
+        )?;
+
+        let mut representation_entity_keys = IndexMap::new();
+        for entity_key in entity_keys {
+            // We remove it from original representation to not hash it both in entity_hash_key and representation_hash_key
+            let (key, value) = representation
+                .remove_entry(entity_key.as_str())
+                .ok_or_else(|| FetchError::MalformedRequest {
+                    reason: format!("can't get entity key {entity_key:?} in representations"),
+                })?;
+            representation_entity_keys.insert(key, value);
+        }
 
-        let typename = opt_type.as_str().unwrap_or("-");
-
-        let hashed_entity_key = hash_entity_key(representation);
+        let hashed_representation = if representation.is_empty() {
+            String::new()
+        } else {
+            hash_other_representation(representation)
+        };
+        let hashed_entity_key = hash_entity_key(&representation_entity_keys);
 
         // the cache key is written to easily find keys matching a prefix for deletion:
         // - entity cache version: current version of the hash
@@ -1346,10 +1428,8 @@ fn extract_cache_keys(
         // - entity key: invalidate a specific entity
         // - query hash: invalidate the entry for a specific query and operation name
         // - additional data: separate cache entries depending on info like authorization status
-        let mut key = String::new();
-        let _ = write!(
-            &mut key,
-            "version:{ENTITY_CACHE_VERSION}:subgraph:{subgraph_name}:type:{typename}:entity:{hashed_entity_key}:hash:{query_hash}:data:{additional_data_hash}"
+        let mut key = format!(
+            "version:{ENTITY_CACHE_VERSION}:subgraph:{subgraph_name}:type:{typename}:entity:{hashed_entity_key}:representation:{hashed_representation}:hash:{query_hash}:data:{additional_data_hash}"
         );
         if is_known_private {
             if let Some(id) = private_id {
@@ -1357,19 +1437,84 @@ fn extract_cache_keys(
             }
         }
 
-        representation
-            .as_object_mut()
-            .map(|o| o.insert(TYPENAME, opt_type));
+        representation.insert(TYPENAME, typename_value);
+        representation_entity_keys
+            .into_iter()
+            .for_each(|(key, val)| {
+                representation.insert(key, val);
+            });
         res.push(key);
     }
     Ok(res)
 }
 
-/// Returns a hash for the given representation, independent of field order. Destructively sorts any objects in the input.
-pub(crate) fn hash_entity_key(representation: &mut Value) -> String {
+fn get_entity_keys_from_supergraph_schema(
+    typename: &str,
+    subgraph_name: &str,
+    supergraph_schema: &Valid<Schema>,
+    subgraph_enums: &HashMap<String, String>,
+) -> Result<impl Iterator<Item = Name>, BoxError> {
+    let entity_keys = supergraph_schema
+        .types
+        .get(typename)
+        .ok_or_else(|| FetchError::MalformedRequest {
+            reason: format!("unknown typename {typename:?} in representations"),
+        })?
+        .directives()
+        .get_all("join__type")
+        .filter_map(move |directive| {
+            let schema_subgraph_name = directive
+                .specified_argument_by_name("graph")
+                .and_then(|arg| arg.as_enum())
+                .and_then(|arg| subgraph_enums.get(arg.as_str()))?;
+
+            if schema_subgraph_name == subgraph_name {
+                let mut parser = Parser::new();
+                directive
+                    .specified_argument_by_name("key")
+                    .and_then(|arg| arg.as_str())
+                    .and_then(|arg| {
+                        parser
+                            .parse_field_set(
+                                supergraph_schema,
+                                NamedType::new(typename).ok()?,
+                                arg,
+                                "entity_caching.graphql",
+                            )
+                            .ok()
+                    })
+            } else {
+                None
+            }
+        })
+        .flat_map(|field_set| {
+            field_set
+                .selection_set
+                .root_fields(&Default::default())
+                .map(|f| f.name.clone())
+                .collect::<Vec<Name>>()
+        });
+
+    Ok(entity_keys)
+}
+
+// Only hash the list of entity keys
+pub(crate) fn hash_entity_key(
+    entity_keys: &IndexMap<ByteString, serde_json_bytes::Value>,
+) -> String {
     // We have to hash the representation because it can contains PII
     let mut digest = Sha256::new();
-    representation.sort_all_objects();
+    digest.update(serde_json::to_string(&entity_keys).unwrap().as_bytes());
+    hex::encode(digest.finalize().as_slice())
+}
+
+// Hash other representation variables except __typename and entity keys
+fn hash_other_representation(
+    representation: &mut serde_json_bytes::Map<ByteString, Value>,
+) -> String {
+    // We had to sort it to be deterministic
+    representation.sort_keys();
+    let mut digest = Sha256::new();
     digest.update(serde_json::to_string(&representation).unwrap().as_bytes());
     hex::encode(digest.finalize().as_slice())
 }
@@ -1620,27 +1765,3 @@ impl Ord for CacheKeyStatus {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_hash_entity_key_ordering() {
-        let mut representations = serde_json_bytes::json!([{
-            "id1": "test",
-            "id2": "test2"
-        }]);
-        let first_hash_key = hash_entity_key(&mut representations);
-        let mut representations = serde_json_bytes::json!([{
-            "id2": "test2",
-            "id1": "test"
-        }]);
-        let second_hash_key = hash_entity_key(&mut representations);
-
-        assert_eq!(
-            first_hash_key, second_hash_key,
-            "these 2 hashes should be equals because ordering doesn't matter"
-        );
-    }
-}
diff --git a/apollo-router/src/plugins/cache/invalidation.rs b/apollo-router/src/plugins/cache/invalidation.rs
index eb3bbc51ea..02acc2e659 100644
--- a/apollo-router/src/plugins/cache/invalidation.rs
+++ b/apollo-router/src/plugins/cache/invalidation.rs
@@ -5,9 +5,11 @@ use fred::error::RedisError;
 use fred::types::Scanner;
 use futures::StreamExt;
 use futures::stream;
+use indexmap::IndexMap;
 use itertools::Itertools;
 use serde::Deserialize;
 use serde::Serialize;
+use serde_json_bytes::ByteString;
 use serde_json_bytes::Value;
 use thiserror::Error;
 use tokio::sync::Semaphore;
@@ -225,7 +227,7 @@ pub(crate) enum InvalidationRequest {
     Entity {
         subgraph: String,
         r#type: String,
-        key: Value,
+        key: IndexMap<ByteString, Value>,
     },
 }
 
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap
index 431614f498..606166a348 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert-5.snap
@@ -1,11 +1,10 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "cached",
     "cache_control": "public"
   },
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap
index e22c6446a5..ad19eb24f1 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert.snap
@@ -1,11 +1,10 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "new",
     "cache_control": "public"
   },
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-2.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-2.snap
new file mode 100644
index 0000000000..7d3bb156a2
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-2.snap
@@ -0,0 +1,7 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: response.response.headers().get(CACHE_CONTROL)
+---
+Some(
+    "public",
+)
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-3.snap
new file mode 100644
index 0000000000..af7d59f620
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-3.snap
@@ -0,0 +1,15 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: response
+---
+{
+  "data": {
+    "topProducts": [
+      {
+        "name": "Test",
+        "shippingEstimate": 15,
+        "price": 150
+      }
+    ]
+  }
+}
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-4.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-4.snap
new file mode 100644
index 0000000000..7d3bb156a2
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-4.snap
@@ -0,0 +1,7 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: response.response.headers().get(CACHE_CONTROL)
+---
+Some(
+    "public",
+)
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-5.snap
new file mode 100644
index 0000000000..6dae33ac49
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-5.snap
@@ -0,0 +1,16 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: cache_keys
+---
+[
+  {
+    "key": "version:1.0:subgraph:inventory:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:representation:7ae1cf67d5c484b3430662edab68bf613043333264470c8f5834fea277b4e060:hash:a7b76558fe61b2f89ee9ea8bac8eee0b702ba1f319eaace576ab6ccdd19cdc80:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "status": "cached",
+    "cache_control": "public"
+  },
+  {
+    "key": "version:1.0:subgraph:products:type:Query:hash:0a9c11d2f86fae30d5fdfe748e0be34c3d69dd26fa6520ee683d4d002e36e62a:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "status": "cached",
+    "cache_control": "public"
+  }
+]
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-6.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-6.snap
new file mode 100644
index 0000000000..af7d59f620
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires-6.snap
@@ -0,0 +1,15 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: response
+---
+{
+  "data": {
+    "topProducts": [
+      {
+        "name": "Test",
+        "shippingEstimate": 15,
+        "price": 150
+      }
+    ]
+  }
+}
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires.snap
new file mode 100644
index 0000000000..00fa8f6177
--- /dev/null
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__insert_with_requires.snap
@@ -0,0 +1,16 @@
+---
+source: apollo-router/src/plugins/cache/tests.rs
+expression: cache_keys
+---
+[
+  {
+    "key": "version:1.0:subgraph:inventory:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:representation:7ae1cf67d5c484b3430662edab68bf613043333264470c8f5834fea277b4e060:hash:a7b76558fe61b2f89ee9ea8bac8eee0b702ba1f319eaace576ab6ccdd19cdc80:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "status": "new",
+    "cache_control": "public"
+  },
+  {
+    "key": "version:1.0:subgraph:products:type:Query:hash:0a9c11d2f86fae30d5fdfe748e0be34c3d69dd26fa6520ee683d4d002e36e62a:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "status": "new",
+    "cache_control": "public"
+  }
+]
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap
index dcb3aff923..fb280ddcc0 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data-3.snap
@@ -1,16 +1,15 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:representation::hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "cached",
     "cache_control": "[REDACTED]"
   },
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "cached",
     "cache_control": "[REDACTED]"
   }
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap
index 3f36da3436..0dd8f8b89b 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__no_data.snap
@@ -1,16 +1,15 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5221ff42b311b757445c096c023cee4fefab5de49735e421c494f1119326317b:representation::hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "new",
     "cache_control": "[REDACTED]"
   },
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:5f460b74668616b2388b1afed7af6557ee4a0d60fbca18f3b4c59b8027035f32:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "new",
     "cache_control": "[REDACTED]"
   }
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap
index 84b0468241..7ccb40c0b9 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-3.snap
@@ -1,11 +1,10 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4",
     "status": "cached",
     "cache_control": "private"
   },
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap
index 84b0468241..7ccb40c0b9 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private-5.snap
@@ -1,11 +1,10 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4",
     "status": "cached",
     "cache_control": "private"
   },
diff --git a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap
index 84066e4eb3..6a15551ec0 100644
--- a/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap
+++ b/apollo-router/src/plugins/cache/snapshots/apollo_router__plugins__cache__tests__private.snap
@@ -1,11 +1,10 @@
 ---
 source: apollo-router/src/plugins/cache/tests.rs
 expression: cache_keys
-snapshot_kind: text
 ---
 [
   {
-    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
+    "key": "version:1.0:subgraph:orga:type:Organization:entity:5811967f540d300d249ab30ae681359a7815fdb5d3dc71a94be1d491006a6b27:representation::hash:4db0b6e27a228df592ab8ae4e6c776bb048cb4d82d191a9eb49c182705781e78:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c",
     "status": "new",
     "cache_control": "private"
   },
diff --git a/apollo-router/src/plugins/cache/tests.rs b/apollo-router/src/plugins/cache/tests.rs
index b4c3fa1817..b487c79326 100644
--- a/apollo-router/src/plugins/cache/tests.rs
+++ b/apollo-router/src/plugins/cache/tests.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 use std::sync::Arc;
 
+use apollo_compiler::Schema;
 use bytes::Bytes;
 use fred::error::RedisErrorKind;
 use fred::mocks::MockCommand;
@@ -28,6 +29,7 @@ use crate::services::subgraph;
 use crate::services::supergraph;
 
 const SCHEMA: &str = include_str!("../../testdata/orga_supergraph.graphql");
+const SCHEMA_REQUIRES: &str = include_str!("../../testdata/supergraph.graphql");
 #[derive(Debug)]
 pub(crate) struct MockStore {
     map: Arc<Mutex<HashMap<Bytes, Bytes>>>,
@@ -152,6 +154,7 @@ impl Mocks for MockStore {
 
 #[tokio::test]
 async fn insert() {
+    let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
     let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
 
     let subgraphs = MockedSubgraphs([
@@ -211,7 +214,7 @@ async fn insert() {
     ]
     .into_iter()
     .collect();
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map)
+    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
         .await
         .unwrap();
 
@@ -242,14 +245,143 @@ async fn insert() {
     insta::assert_json_snapshot!(response);
 
     // Now testing without any mock subgraphs, all the data should come from the cache
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), HashMap::new())
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
+
+    let service = TestHarness::builder()
+        .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
+        .unwrap()
+        .schema(SCHEMA)
+        .extra_plugin(entity_cache)
+        .build_supergraph()
+        .await
+        .unwrap();
+
+    let request = supergraph::Request::fake_builder()
+        .query(query)
+        .context(Context::new())
+        .build()
+        .unwrap();
+    let mut response = service.oneshot(request).await.unwrap();
+
+    insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
+    let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
+    let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
+    cache_keys.sort();
+    insta::assert_json_snapshot!(cache_keys);
+
+    let response = response.next_response().await.unwrap();
+
+    insta::assert_json_snapshot!(response);
+}
+
+#[tokio::test]
+async fn insert_with_requires() {
+    let valid_schema =
+        Arc::new(Schema::parse_and_validate(SCHEMA_REQUIRES, "test.graphql").unwrap());
+    let query = "query { topProducts { name shippingEstimate price } }";
+
+    let subgraphs = MockedSubgraphs([
+        ("products", MockSubgraph::builder().with_json(
+                serde_json::json!{{"query":"{ topProducts { __typename upc name price weight } }"}},
+                serde_json::json!{{"data": {"topProducts": [{
+                    "__typename": "Product",
+                    "upc": "1",
+                    "name": "Test",
+                    "price": 150,
+                    "weight": 5
+                }]}}}
+        ).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build()),
+        ("inventory", MockSubgraph::builder().with_json(
+            serde_json::json!{{
+                "query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { shippingEstimate } } }",
+                "variables": {
+                    "representations": [
+                        {
+                            "price": 150,
+                            "weight": 5,
+                            "upc": "1",
+                            "__typename": "Product"
+                        }
+                    ]
+            }}},
+            serde_json::json!{{"data": {
+                "_entities": [{
+                    "shippingEstimate": 15
+                }]
+            }}}
+        ).with_header(CACHE_CONTROL, HeaderValue::from_static("public")).build())
+    ].into_iter().collect());
+
+    let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
+        .await
+        .unwrap();
+    let map = [
+        (
+            "products".to_string(),
+            Subgraph {
+                redis: None,
+                private_id: Some("sub".to_string()),
+                enabled: true,
+                ttl: None,
+                ..Default::default()
+            },
+        ),
+        (
+            "inventory".to_string(),
+            Subgraph {
+                redis: None,
+                private_id: Some("sub".to_string()),
+                enabled: true,
+                ttl: None,
+                ..Default::default()
+            },
+        ),
+    ]
+    .into_iter()
+    .collect();
+    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
         .await
         .unwrap();
 
     let service = TestHarness::builder()
         .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
         .unwrap()
-        .schema(SCHEMA)
+        .schema(SCHEMA_REQUIRES)
+        .extra_plugin(entity_cache)
+        .extra_plugin(subgraphs)
+        .build_supergraph()
+        .await
+        .unwrap();
+
+    let request = supergraph::Request::fake_builder()
+        .query(query)
+        .context(Context::new())
+        .build()
+        .unwrap();
+    let mut response = service.oneshot(request).await.unwrap();
+    let cache_keys: CacheKeysContext = response.context.get(CONTEXT_CACHE_KEYS).unwrap().unwrap();
+    let mut cache_keys: Vec<CacheKeyContext> = cache_keys.into_values().flatten().collect();
+    cache_keys.sort();
+    insta::assert_json_snapshot!(cache_keys);
+
+    insta::assert_debug_snapshot!(response.response.headers().get(CACHE_CONTROL));
+    let response = response.next_response().await.unwrap();
+
+    insta::assert_json_snapshot!(response);
+
+    // Now testing without any mock subgraphs, all the data should come from the cache
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
+
+    let service = TestHarness::builder()
+        .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
+        .unwrap()
+        .schema(SCHEMA_REQUIRES)
         .extra_plugin(entity_cache)
         .build_supergraph()
         .await
@@ -275,6 +407,7 @@ async fn insert() {
 
 #[tokio::test]
 async fn no_cache_control() {
+    let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
     let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
 
     let subgraphs = MockedSubgraphs([
@@ -310,9 +443,10 @@ async fn no_cache_control() {
     let redis_cache = RedisCacheStorage::from_mocks(Arc::new(MockStore::new()))
         .await
         .unwrap();
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), HashMap::new())
-        .await
-        .unwrap();
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
 
     let service = TestHarness::builder()
         .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
@@ -337,9 +471,10 @@ async fn no_cache_control() {
     insta::assert_json_snapshot!(response);
 
     // Now testing without any mock subgraphs, all the data should come from the cache
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), HashMap::new())
-        .await
-        .unwrap();
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
 
     let service = TestHarness::builder()
         .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true } }))
@@ -366,6 +501,7 @@ async fn no_cache_control() {
 #[tokio::test]
 async fn private() {
     let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }";
+    let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
 
     let subgraphs = MockedSubgraphs([
         ("user", MockSubgraph::builder().with_json(
@@ -425,7 +561,7 @@ async fn private() {
     ]
     .into_iter()
     .collect();
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map)
+    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
         .await
         .unwrap();
 
@@ -512,6 +648,7 @@ async fn private() {
 #[tokio::test]
 async fn no_data() {
     let query = "query { currentUser { allOrganizations { id name } } }";
+    let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
 
     let subgraphs = MockedSubgraphs([
         ("user", MockSubgraph::builder().with_json(
@@ -582,7 +719,7 @@ async fn no_data() {
     ]
     .into_iter()
     .collect();
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map)
+    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
         .await
         .unwrap();
 
@@ -618,9 +755,10 @@ async fn no_data() {
     let response = response.next_response().await.unwrap();
     insta::assert_json_snapshot!(response);
 
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), HashMap::new())
-        .await
-        .unwrap();
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
 
     let subgraphs = MockedSubgraphs(
         [(
@@ -698,7 +836,7 @@ async fn no_data() {
 #[tokio::test]
 async fn missing_entities() {
     let query = "query { currentUser { allOrganizations { id name } } }";
-
+    let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap());
     let subgraphs = MockedSubgraphs([
         ("user", MockSubgraph::builder().with_json(
                 serde_json::json!{{"query":"{currentUser{allOrganizations{__typename id}}}"}},
@@ -770,7 +908,7 @@ async fn missing_entities() {
     ]
     .into_iter()
     .collect();
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map)
+    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), map, valid_schema.clone())
         .await
         .unwrap();
 
@@ -793,9 +931,10 @@ async fn missing_entities() {
     let response = response.next_response().await.unwrap();
     insta::assert_json_snapshot!(response);
 
-    let entity_cache = EntityCache::with_mocks(redis_cache.clone(), HashMap::new())
-        .await
-        .unwrap();
+    let entity_cache =
+        EntityCache::with_mocks(redis_cache.clone(), HashMap::new(), valid_schema.clone())
+            .await
+            .unwrap();
 
     let subgraphs = MockedSubgraphs([
             ("user", MockSubgraph::builder().with_json(
diff --git a/apollo-router/src/query_planner/mod.rs b/apollo-router/src/query_planner/mod.rs
index 3a739e1101..f2f4a73da8 100644
--- a/apollo-router/src/query_planner/mod.rs
+++ b/apollo-router/src/query_planner/mod.rs
@@ -18,7 +18,7 @@ mod labeler;
 mod plan;
 pub(crate) mod query_planner_service;
 pub(crate) mod rewrites;
-mod selection;
+pub(crate) mod selection;
 mod subgraph_context;
 pub(crate) mod subscription;
 
diff --git a/apollo-router/src/query_planner/selection.rs b/apollo-router/src/query_planner/selection.rs
index be3fc19958..19937df0eb 100644
--- a/apollo-router/src/query_planner/selection.rs
+++ b/apollo-router/src/query_planner/selection.rs
@@ -729,9 +729,9 @@ mod tests {
           @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
           query: Query
         }
-        
+
         directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
-        
+
         directive @join__field(
           graph: join__Graph
           requires: join__FieldSet
@@ -741,14 +741,14 @@ mod tests {
           override: String
           usedOverridden: Boolean
         ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
-        
+
         directive @join__graph(name: String!, url: String!) on ENUM_VALUE
-        
+
         directive @join__implements(
           graph: join__Graph!
           interface: String!
         ) repeatable on OBJECT | INTERFACE
-        
+
         directive @join__type(
           graph: join__Graph!
           key: join__FieldSet
@@ -756,27 +756,27 @@ mod tests {
           resolvable: Boolean! = true
           isInterfaceObject: Boolean! = false
         ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
-        
+
         directive @join__unionMember(
           graph: join__Graph!
           member: String!
         ) repeatable on UNION
-        
+
         directive @link(
           url: String
           as: String
           for: link__Purpose
           import: [link__Import]
         ) repeatable on SCHEMA
-        
+
         scalar join__FieldSet
-        
+
         enum join__Graph {
             TEST @join__graph(name: "test", url: "http://localhost:4001/graphql")
         }
-        
+
         scalar link__Import
-        
+
         enum link__Purpose {
           SECURITY
           EXECUTION
diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs
index f44195a5c7..e6463f3e79 100644
--- a/apollo-router/tests/integration/redis.rs
+++ b/apollo-router/tests/integration/redis.rs
@@ -461,7 +461,7 @@ async fn entity_cache_basic() -> Result<(), BoxError> {
     let v: Value = serde_json::from_str(&s).unwrap();
     insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap());
 
-    let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap();
+    let s: String = client.get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:representation::hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c").await.unwrap();
     let v: Value = serde_json::from_str(&s).unwrap();
     insta::assert_json_snapshot!(v.as_object().unwrap().get("data").unwrap());
 
@@ -573,7 +573,7 @@ async fn entity_cache_basic() -> Result<(), BoxError> {
     insta::assert_json_snapshot!(response);
 
     let s:String = client
-        .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
+        .get("version:1.0:subgraph:reviews:type:Product:entity:d9a4cd73308dd13ca136390c10340823f94c335b9da198d2339c886c738abf0d:representation::hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
         .await
         .unwrap();
     let v: Value = serde_json::from_str(&s).unwrap();
@@ -823,7 +823,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> {
     );
 
     let s: String = client
-        .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
+        .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:representation::hash:b9b8a9c94830cf56329ec2db7d7728881a6ba19cc1587710473e732e775a5870:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
         .await
         .unwrap();
     let v: Value = serde_json::from_str(&s).unwrap();
@@ -867,7 +867,7 @@ async fn entity_cache_authorization() -> Result<(), BoxError> {
     insta::assert_json_snapshot!(response);
 
     let s:String = client
-          .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:hash:572a2cdde770584306cd0def24555773323b1738e9a303c7abc1721b6f9f2ec4:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
+          .get("version:1.0:subgraph:reviews:type:Product:entity:4911f7a9dbad8a47b8900d65547503a2f3c0359f65c0bc5652ad9b9843281f66:representation::hash:572a2cdde770584306cd0def24555773323b1738e9a303c7abc1721b6f9f2ec4:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
           .await
           .unwrap();
     let v: Value = serde_json::from_str(&s).unwrap();
@@ -880,6 +880,30 @@ async fn entity_cache_authorization() -> Result<(), BoxError> {
             }]
         }}
     );
+    let s:String = client
+          .get("version:1.0:subgraph:reviews:type:Product:entity:4e48855987eae27208b466b941ecda5fb9b88abc03301afef6e4099a981889e9:representation::hash:572a2cdde770584306cd0def24555773323b1738e9a303c7abc1721b6f9f2ec4:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c")
+          .await
+          .unwrap();
+    let v: Value = serde_json::from_str(&s).unwrap();
+    assert_eq!(
+        v.as_object().unwrap().get("data").unwrap(),
+        &json! {{
+            "reviews": [{
+                "body": "I can sit on it",
+                "author": {
+                    "__typename": "User",
+                    "id": "1"
+                }
+            },
+            {
+                "body": "I can eat on it",
+                "author": {
+                    "__typename": "User",
+                    "id": "2"
+                }
+            }]
+        }}
+    );
 
     let context = Context::new();
     context


Reviews:
user: login: SimonSapin
body: Looks good so far, but since it’s draft I assume you want to make more changes?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T19:48:41Z
user: login: SimonSapin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:12:25Z

Review comments:
diff_hunk: @@ -1344,22 +1410,55 @@ fn extract_cache_keys(
         // - query hash: invalidate the entry for a specific query and operation name
         // - additional data: separate cache entries depending on info like authorization status
         let mut key = String::new();
-        let _ = write!(&mut key, "version:{ENTITY_CACHE_VERSION}:subgraph:{subgraph_name}:type:{typename}:entity:{hashed_entity_key}:hash:{query_hash}:data:{additional_data_hash}");
+        let _ = write!(&mut key, "version:{ENTITY_CACHE_VERSION}:subgraph:{subgraph_name}:type:{typename}:entity:{hashed_entity_key}:representation:{hashed_representation}:hash:{query_hash}:data:{additional_data_hash}");
         if is_known_private {
             if let Some(id) = private_id {
                 let _ = write!(&mut key, ":{id}");
             }
         }
 
-        representation
-            .as_object_mut()
-            .map(|o| o.insert(TYPENAME, opt_type));
+        representation.insert(TYPENAME, typename_value);
+        representation_entity_keys
+            .into_iter()
+            .for_each(|(key, val)| {
+                representation.insert(key, val);
+            });
         res.push(key);
     }
     Ok(res)
 }
 
-pub(crate) fn hash_entity_key(representation: &Value) -> String {
+/// Use it specifically for keys field set because we don't check for fragment spread
+fn get_root_field_names(selection_set: &SelectionSet) -> Vec<Name> {
path: apollo-router/src/plugins/cache/entity.rs
user: login: SimonSapin
body: With apollo-compiler 1.27.0 released today `selection_set.root_fields()` returns an iterator that can replace this function
created_at: 2025-03-04T19:45:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,6 +152,9 @@ impl Mocks for MockStore {
 
 #[tokio::test]
 async fn insert() {
+    let mut parser = apollo_compiler::parser::Parser::new();
+    let schema = parser.parse_schema(SCHEMA, "test.graphql").unwrap();
+    let valid_schema = Arc::new(schema.validate().unwrap());
path: apollo-router/src/plugins/cache/tests.rs
user: login: SimonSapin
body: If you’re not changing `Parser` parameters you can use `Schema::parse_and_validate()`

Same a few more times below
created_at: 2025-03-04T19:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -500,10 +514,33 @@ impl EntityCache {
                 concurrent_requests: 10,
             })),
             invalidation,
+            subgraph_enums: Arc::new(get_subgraph_enums(&supergraph_schema)),
+            supergraph_schema,
         })
     }
 }
 
+/// Get the map of subgraph enum variant mapped with subgraph name
+fn get_subgraph_enums(supergraph_schema: &Valid<Schema>) -> HashMap<String, String> {
+    let mut subgraph_enums = HashMap::new();
+    if let Some(graph_enum) = supergraph_schema.get_enum("join__Graph") {
+        subgraph_enums.extend(graph_enum.values.iter().filter_map(
+            |(enum_name, enum_value_def)| {
+                let subgraph_name = enum_value_def
+                    .directives
+                    .get("join__graph")
+                    .and_then(|directive| directive.specified_argument_by_name("name"))
+                    .and_then(|arg| arg.as_str())?
+                    .to_string();
path: apollo-router/src/plugins/cache/entity.rs
user: login: SimonSapin
body: Nit (feel free to ignore): I think this might work:
```suggestion
                    .get("join__graph")?
                    .specified_argument_by_name("name")?
                    .as_str()?
                    .to_string();
```
created_at: 2025-03-07T11:09:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: svc-apollo-docs
created_at: 2025-03-03T11:03:52Z
body: ### ✅ Docs preview has no changes

The preview was not built because there were no changes.

**Build ID**: e6aad957d00f4c0c4d5bd660
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T11:04:03Z
body: @bnjjj, please consider creating a changeset entry in `/.changesets/`. [These instructions](https://github.com/apollographql/router/blob/HEAD/.changesets/README.md) describe the process and tooling.
author_association: CONTRIBUTOR
user: login: bnjjj
created_at: 2025-03-05T10:16:38Z
body: > Looks good so far, but since it’s draft I assume you want to make more changes?
@SimonSapin yes tests
author_association: CONTRIBUTOR
user: login: abernix
created_at: 2025-03-07T17:53:25Z
body: @bnjjj The test failure(s) here looks potentially related, so I'm going to disable auto-merge while you take a look.
author_association: MEMBER
user: login: bnjjj
created_at: 2025-03-10T09:51:16Z
body: Good catch @abernix . I fixed it :) thank you :)

author_association: CONTRIBUTOR