DocsRecipesFederation Design Patterns

Apollo Federation Design Patterns

Best practices and common patterns for implementing Apollo Federation with x-graphql converters.


Table of Contents

  1. Entity Resolution Patterns
  2. Foreign Key Pattern (Recommended)
  3. Non-Resolvable References
  4. Composite Keys
  5. Entity Extensions
  6. Resolver Implementation
  7. Common Mistakes
  8. Testing & Validation

Entity Resolution Patterns

Apollo Federation allows you to split your GraphQL schema across multiple services (subgraphs). Entities are types that can be referenced and extended across subgraph boundaries.

Key Concepts:

  • Entity: A type with @key directive that can be referenced from other subgraphs
  • Foreign Key: A field storing the key value of a related entity
  • Reference Resolution: How the gateway fetches entity data across subgraphs
  • @external: Marks fields defined in another subgraph
  • @provides: Tells the gateway which fields this subgraph can provide
  • @requires: Declares dependencies on other fields for resolution

Foreign Key Pattern

The Problem

Without foreign key fields, the gateway cannot construct the query to fetch entity data from the owning service.

The Solution

Store foreign key fields on the referencing entity that match the @key fields of the referenced entity.

Example: Product Reviews

Products Service (Owner)

{
  "definitions": {
    "Product": {
      "type": "object",
      "x-graphql-type-name": "Product",
      "x-graphql-federation-keys": ["upc"],
      "properties": {
        "upc": {
          "type": "string",
          "x-graphql-field-type": "ID",
          "x-graphql-field-non-null": true
        },
        "name": { "type": "string" },
        "price": { "type": "number" }
      },
      "required": ["upc"]
    }
  }
}

Generated SDL:

type Product @key(fields: "upc") {
  upc: ID!
  name: String
  price: Float
}

Reviews Service (Referencer)

{
  "definitions": {
    "Review": {
      "type": "object",
      "x-graphql-federation-keys": ["id"],
      "properties": {
        "id": {
          "type": "string",
          "x-graphql-field-type": "ID",
          "x-graphql-field-non-null": true
        },
        "product_upc": {
          "type": "string",
          "x-graphql-field-name": "productUpc",
          "x-graphql-field-type": "ID",
          "x-graphql-field-non-null": true
        },
        "rating": { "type": "integer" },
        "body": { "type": "string" },
        "product": {
          "type": "object",
          "x-graphql-field-type": "Product",
          "x-graphql-field-non-null": true
        }
      },
      "required": ["id", "product_upc", "product"]
    }
  }
}

Generated SDL:

type Review @key(fields: "id") {
  id: ID!
  productUpc: ID!
  rating: Int
  body: String
  product: Product!
}

Why This Works

  1. Gateway Query Path: When resolving Review.product, the gateway reads Review.productUpc, constructs Product(upc: "..."), and fetches from the Products service.
  2. No Ambiguity: The foreign key explicitly stores which product the review belongs to.

Non-Resolvable References

Use resolvable: false when you only need an entity stub — no additional field fetching.

{
  "Product": {
    "type": "object",
    "x-graphql-federation-keys": [{ "fields": "upc", "resolvable": false }],
    "properties": {
      "upc": {
        "type": "string",
        "x-graphql-field-type": "ID",
        "x-graphql-field-non-null": true
      }
    }
  }
}

Generated SDL:

type Product @key(fields: "upc", resolvable: false) {
  upc: ID!
}

Note: With resolvable: false, the gateway will not fetch additional Product fields. Queries requesting those fields will fail.


Composite Keys

Multiple Fields

{
  "User": {
    "x-graphql-federation-keys": ["id organizationId"],
    "properties": {
      "id": { "x-graphql-field-type": "ID", "x-graphql-field-non-null": true },
      "organization_id": {
        "x-graphql-field-name": "organizationId",
        "x-graphql-field-type": "ID",
        "x-graphql-field-non-null": true
      }
    }
  }
}

Generated SDL:

type User @key(fields: "id organizationId") {
  id: ID!
  organizationId: ID!
}

Nested Object Keys

{
  "User": {
    "x-graphql-federation-keys": ["id organization { id }"],
    "properties": {
      "id": { "x-graphql-field-type": "ID", "x-graphql-field-non-null": true },
      "organization": {
        "x-graphql-field-type": "Organization",
        "x-graphql-field-non-null": true
      }
    }
  }
}

Generated SDL:

type User @key(fields: "id organization { id }") {
  id: ID!
  organization: Organization!
}

Multiple Keys

{
  "Product": {
    "x-graphql-federation-keys": ["upc", "sku"],
    "properties": {
      "upc": { "x-graphql-field-type": "ID", "x-graphql-field-non-null": true },
      "sku": { "x-graphql-field-type": "ID", "x-graphql-field-non-null": true }
    }
  }
}

Generated SDL:

type Product @key(fields: "upc") @key(fields: "sku") {
  upc: ID!
  sku: ID!
}

Entity Extensions

Using @external and @requires

{
  "Product": {
    "x-graphql-federation-keys": ["upc"],
    "x-graphql-federation-extends": true,
    "properties": {
      "upc": { "x-graphql-field-type": "ID", "x-graphql-field-non-null": true },
      "price": { "x-graphql-federation-external": true },
      "weight": { "x-graphql-federation-external": true },
      "shipping_estimate": {
        "x-graphql-field-name": "shippingEstimate",
        "x-graphql-field-type": "Float",
        "x-graphql-federation-requires": "price weight"
      }
    }
  }
}

Generated SDL:

type Product @key(fields: "upc") {
  upc: ID!
  price: Float @external
  weight: Float @external
  shippingEstimate: Float @requires(fields: "price weight")
}

Using @provides

{
  "Review": {
    "properties": {
      "product": {
        "x-graphql-field-type": "Product",
        "x-graphql-field-non-null": true,
        "x-graphql-federation-provides": "name"
      }
    }
  }
}

Generated SDL:

type Review {
  product: Product! @provides(fields: "name")
}

Resolver Implementation

__resolveReference

const resolvers = {
  Product: {
    __resolveReference(reference) {
      return fetchProductByUpc(reference.upc);
    },
  },
};

With DataLoader (recommended):

const resolvers = {
  Product: {
    __resolveReference(reference, context) {
      return context.productLoader.load(reference.upc);
    },
  },
};

Foreign Key Resolvers

const resolvers = {
  Review: {
    product(review, args, context) {
      return context.productLoader.load(review.productUpc);
    },
    author(review, args, context) {
      return context.userLoader.load(review.authorEmail);
    },
  },
};

Common Mistakes

1. Missing Foreign Keys ❌

// ❌ No product_upc = gateway cannot resolve Product
{ "Review": { "properties": { "product": { "x-graphql-field-type": "Product" } } } }
 
// ✅ Add the foreign key
{
  "Review": {
    "properties": {
      "product_upc": { "x-graphql-field-name": "productUpc", "x-graphql-field-type": "ID", "x-graphql-field-non-null": true },
      "product": { "x-graphql-field-type": "Product" }
    },
    "required": ["product_upc"]
  }
}

2. Key Field Marked @external in Extension ❌

Key fields in an extension must not be marked @external — they identify the entity.

3. Circular Dependencies ❌

Restructure so one service owns the entity; others only extend it.

4. Missing __resolveReference ❌

Always implement __resolveReference for every entity type.

5. Mismatched Key Types ❌

Ensure key field types match across all services (e.g., both use ID, not one String and one ID).


Testing & Validation

Composition Validation

node scripts/validate-federation-composition.js

Common Composition Errors

ErrorCauseFix
SATISFIABILITY_ERRORMissing foreign keys or @external on keysAdd foreign key fields
EXTERNAL_MISSING_ON_BASE@external on field not in owning serviceRemove @external or add field to owner
KEY_FIELDS_MISSING_EXTERNALExtension declares new key fieldsUse same keys as owning service

Manual Testing

query {
  product(upc: "12345") {
    name
    reviews {
      rating
      author {
        name
      }
    }
  }
}