# Part 6: GraphQL Federation — TypeScript Gateway and Python Subgraph

## Splitting a Monolith Without a Big Bang

When I first built a GraphQL API to serve a React frontend and a mobile app, everything was in a single Apollo Server. One schema, one deployment, one team owning everything. It worked fine until a second team needed their own domain added to the graph — then the monolith started to become a coordination problem.

Apollo Federation solves this by letting each service own its slice of the graph. Services are **subgraphs** and are unified behind a **supergraph gateway** (Apollo Router). The client still sends queries to one endpoint. The gateway decomposes those queries, fans them out to the right subgraphs, and assembles the final response.

The architecture we build in this part:

```
┌─────────────────────────────────────────────────────────┐
│  Apollo Router (supergraph gateway — localhost:4000)    │
└────────────────┬────────────────────────────────────────┘
                 │
     ┌───────────┴────────────┐
     │                        │
┌────▼────────────────┐  ┌────▼──────────────────────┐
│  TypeScript         │  │  Python (Strawberry)       │
│  Product Subgraph   │  │  Inventory Subgraph        │
│  localhost:4001     │  │  localhost:4002             │
│  @apollo/subgraph   │  │  strawberry.federation     │
└─────────────────────┘  └────────────────────────────┘
```

The client queries Apollo Router. The router internally routes:

* `product`, `products`, `createProduct` → TypeScript Product Subgraph
* `stockLevels`, `replenishmentSchedule` → Python Inventory Subgraph

Both subgraphs know about the `Product` entity, and federation allows the Python service to *extend* the TypeScript service's `Product` type with additional fields.

## Apollo Federation v2 Key Concepts

Before the code, a glossary:

| Term                   | Meaning                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| **Subgraph**           | An individual GraphQL service that contributes types and fields to the supergraph          |
| **Supergraph**         | The combined schema produced by composing all subgraphs                                    |
| **Entity**             | A type with an `@key` directive — it can be referenced and extended across subgraphs       |
| **`@key`**             | Marks a field (or set of fields) as the unique identifier for an entity                    |
| **`@external`**        | Marks a field that is defined in another subgraph but referenced here                      |
| **`@requires`**        | Declares that a resolver needs fields from the owning subgraph before it can run           |
| **`@provides`**        | Declares that a resolver will supply fields normally fetched from another subgraph         |
| **Reference resolver** | A special `__resolveReference` function that reconstructs an entity from its `@key` fields |
| **Apollo Router**      | The Rust-based high-performance gateway that routes queries across subgraphs               |

## Step 1 — TypeScript Product Subgraph

Convert the Apollo Server from Part 3 into a federation subgraph.

```bash
npm install @apollo/subgraph
```

Add federation directives to the SDL:

```typescript
// src/schema.ts
import { gql } from 'graphql-tag';

export const typeDefs = gql`
  # Federation v2 link declaration
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@external", "@requires", "@provides", "@shareable"])

  scalar DateTime
  scalar UUID

  type Product @key(fields: "id") {
    id: UUID!
    name: String!
    slug: String!
    description: String
    price: Float!
    currency: String!
    inStock: Boolean!
    stockCount: Int!
    category: Category!
    tags: [String!]!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Category @key(fields: "id") {
    id: UUID!
    name: String!
    slug: String!
    products(first: Int, after: String): ProductConnection!
  }

  type ProductEdge {
    cursor: String!
    node: Product!
  }

  type PageInfo {
    startCursor: String
    endCursor: String
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }

  type ProductConnection {
    edges: [ProductEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }

  type Query {
    product(id: UUID!): Product
    products(first: Int, after: String): ProductConnection!
  }

  type Mutation {
    createProduct(input: CreateProductInput!): Product!
    updateProduct(id: UUID!, input: UpdateProductInput!): Product!
    deleteProduct(id: UUID!): Boolean!
    updateStock(productId: UUID!, quantity: Int!): Product!
  }

  input CreateProductInput {
    name: String!
    slug: String!
    description: String
    price: Float!
    currency: String!
    categoryId: UUID!
    tags: [String!]
  }

  input UpdateProductInput {
    name: String
    description: String
    price: Float
    categoryId: UUID
    tags: [String!]
  }
`;
```

Add reference resolvers (called by the gateway when it needs to reconstruct a `Product` entity):

```typescript
// src/resolvers/product.ts (additions for federation)
export const productResolvers = {
  // ...existing Query and Mutation resolvers from Part 3...

  Product: {
    __resolveReference: async (
      reference: { id: string },
      ctx: AppContext,
    ) => {
      // The gateway sends just { __typename: 'Product', id: '...' }
      // We load the full object from our database
      return ctx.prisma.product.findUnique({ where: { id: reference.id } });
    },

    inStock: (product: { stockCount: number }) => product.stockCount > 0,

    category: (product: { categoryId: string }, _args: unknown, ctx: AppContext) => {
      return ctx.loaders.category.load(product.categoryId);
    },
  },

  Category: {
    __resolveReference: async (
      reference: { id: string },
      ctx: AppContext,
    ) => {
      return ctx.prisma.category.findUnique({ where: { id: reference.id } });
    },
  },
};
```

Replace `ApolloServer` with a federation-aware build:

```typescript
// src/index.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { createContext } from './context';

async function startServer() {
  const app = express();
  const httpServer = http.createServer(app);

  // buildSubgraphSchema adds federation query support (__service, _entities)
  const schema = buildSubgraphSchema({ typeDefs, resolvers });

  const server = new ApolloServer({
    schema,
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
  });

  await server.start();

  app.use('/graphql', express.json(), expressMiddleware(server, {
    context: async ({ req }) => createContext({ req }),
  }));

  httpServer.listen(4001, () => {
    console.log('Product subgraph running at http://localhost:4001/graphql');
  });
}

startServer().catch(console.error);
```

## Step 2 — Python Inventory Subgraph with Strawberry

The Python service owns inventory data (warehouse stock levels, replenishment schedules). It knows about `Product` entities from the TypeScript service and extends them with inventory fields.

```bash
pip install "strawberry-graphql[fastapi,debug-server]"
pip install strawberry-graphql-django  # optional, if using Django
pip install sqlalchemy asyncpg         # for async DB
```

### Strawberry Schema with Federation

```python
# src/schema.py
from __future__ import annotations
import strawberry
import strawberry.federation
from strawberry.federation.schema_directives import Key
from strawberry import ID
from typing import Optional, Annotated
from datetime import datetime


# ─── Types ───────────────────────────────────────────────────────────────────

@strawberry.federation.type(keys=["id"])
class Product:
    """
    A stub type for the Product entity owned by the TypeScript subgraph.
    We extend it here with inventory-specific fields.
    """
    id: strawberry.ID = strawberry.federation.field(external=True)

    @strawberry.federation.field
    async def stock_levels(self, info: strawberry.types.Info) -> list[StockLevel]:
        """Inventory field added by this subgraph."""
        return await info.context["db"].get_stock_levels(product_id=self.id)

    @strawberry.federation.field
    async def replenishment_schedule(
        self, info: strawberry.types.Info
    ) -> Optional[ReplenishmentSchedule]:
        return await info.context["db"].get_replenishment_schedule(product_id=self.id)

    @classmethod
    def resolve_reference(cls, id: strawberry.ID) -> Product:
        # Called by the gateway to reconstruct the Product stub
        return cls(id=id)


@strawberry.type
class StockLevel:
    warehouse_id: strawberry.ID
    warehouse_name: str
    quantity: int
    reserved_quantity: int
    available_quantity: int
    updated_at: datetime


@strawberry.type
class ReplenishmentSchedule:
    product_id: strawberry.ID
    reorder_point: int
    reorder_quantity: int
    lead_time_days: int
    next_order_date: Optional[datetime]


# ─── Inputs ───────────────────────────────────────────────────────────────────

@strawberry.input
class UpdateStockInput:
    warehouse_id: str
    product_id: str
    quantity: int


# ─── Resolvers ────────────────────────────────────────────────────────────────

@strawberry.type
class Query:
    @strawberry.field
    async def stock_level(
        self,
        product_id: strawberry.ID,
        warehouse_id: strawberry.ID,
        info: strawberry.types.Info,
    ) -> Optional[StockLevel]:
        return await info.context["db"].get_stock_level(
            product_id=product_id, warehouse_id=warehouse_id
        )


@strawberry.type
class Mutation:
    @strawberry.mutation
    async def update_stock(
        self, input: UpdateStockInput, info: strawberry.types.Info
    ) -> StockLevel:
        return await info.context["db"].update_stock(
            product_id=input.product_id,
            warehouse_id=input.warehouse_id,
            quantity=input.quantity,
        )


# ─── Schema ───────────────────────────────────────────────────────────────────

schema = strawberry.federation.Schema(
    query=Query,
    mutation=Mutation,
    types=[Product, StockLevel, ReplenishmentSchedule],
    enable_federation_2=True,
)
```

### FastAPI Entry Point

```python
# src/main.py
import os
from fastapi import FastAPI, Request
from strawberry.fastapi import GraphQLRouter
from .schema import schema
from .database import AsyncDatabase

db = AsyncDatabase(dsn=os.environ.get("DATABASE_URL", "postgresql+asyncpg://localhost/inventory"))


async def get_context(request: Request) -> dict:
    return {
        "db": db,
        "request": request,
        "user": None,  # auth added same way as Part 5
    }


app = FastAPI(title="Inventory Subgraph")

graphql_app = GraphQLRouter(schema, context_getter=get_context)
app.include_router(graphql_app, prefix="/graphql")


@app.get("/health")
async def health():
    return {"status": "ok"}
```

## Step 3 — Apollo Router

Apollo Router is a Rust binary that acts as the supergraph gateway. It reads a supergraph schema composed from all subgraph SDLs.

### Install Rover CLI and Apollo Router

```bash
# Install Rover (Apollo's schema management CLI)
curl -sSL https://rover.apollo.dev/nix/latest | sh

# Install Apollo Router
curl -sSL https://router.apollo.dev/download/nix/latest | sh
```

### Compose the Supergraph Schema

```yaml
# supergraph.yaml
federation_version: =2.0.0

subgraphs:
  product:
    routing_url: http://localhost:4001/graphql
    schema:
      subgraph_url: http://localhost:4001/graphql

  inventory:
    routing_url: http://localhost:4002/graphql
    schema:
      subgraph_url: http://localhost:4002/graphql
```

```bash
# Both subgraphs must be running for this to fetch their schemas
rover supergraph compose --config supergraph.yaml > supergraph-schema.graphql
```

### Router Config

```yaml
# router.yaml
supergraph:
  path: /graphql

health_check:
  enabled: true
  listen: 127.0.0.1:8088
  path: /health

cors:
  origins:
    - http://localhost:3000

headers:
  all:
    request:
      - propagate:
          named: Authorization
```

### Start the Router

```bash
./router --config router.yaml --supergraph supergraph-schema.graphql
```

## Step 4 — Cross-Service Query in Action

With both subgraphs and the router running, this query works:

```graphql
query ProductWithInventory($id: UUID!) {
  product(id: $id) {
    id
    name
    price

    # ↑ Product fields — served by TypeScript subgraph

    stockLevels {
      warehouseName
      availableQuantity
    }

    replenishmentSchedule {
      reorderPoint
      leadTimeDays
      nextOrderDate
    }

    # ↑ Inventory fields — served by Python subgraph
  }
}
```

The router's query plan:

1. Route `product(id: $id) { id name price }` → TypeScript Product Subgraph
2. Receive the product's `id` from step 1
3. Route `{ _entities(representations: [{__typename: "Product", id: "..."}]) { ... on Product { stockLevels { ... } replenishmentSchedule { ... } } } }` → Python Inventory Subgraph
4. Merge both responses and return a single result to the client

The client is completely unaware that two services were involved.

## Docker Compose for Local Development

```yaml
# docker-compose.yml
services:
  product-service:
    build: ./graphql-product-service
    ports:
      - "4001:4001"
    environment:
      DATABASE_URL: postgresql://postgres:password@postgres:5432/products
      JWT_SECRET: dev-secret
    depends_on:
      - postgres

  inventory-service:
    build: ./inventory-service
    ports:
      - "4002:4002"
    environment:
      DATABASE_URL: postgresql+asyncpg://postgres:password@postgres:5432/inventory
    depends_on:
      - postgres

  router:
    image: ghcr.io/apollographql/router:v1.52.0
    ports:
      - "4000:4000"
    volumes:
      - ./router.yaml:/dist/config/router.yaml
      - ./supergraph-schema.graphql:/dist/config/supergraph.graphql
    command: --config /dist/config/router.yaml --supergraph /dist/config/supergraph.graphql
    depends_on:
      - product-service
      - inventory-service

  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: password
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:
```

## What's Next

You now have a working federated GraphQL supergraph:

* TypeScript Apollo Server as a subgraph — owns `Product` and `Category`
* Python Strawberry as a subgraph — owns `StockLevel` and extends `Product`
* Apollo Router composing both into a single endpoint

In [Part 7](https://blog.htunnthuthu.com/getting-started/programming/graphql-101/part-7-testing-performance-deployment), we test both subgraphs in isolation, measure and fix query performance, and set up a production deployment.
