Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Composable architecture with Graph

Design flexible, scalable composable architecture patterns using Optimizely Graph as the foundation for modern digital experiences.

Composable architecture structures digital experiences as modular, interchangeable components rather than monolithic platforms. Optimizely Graph serves as the content layer in composable systems, providing a unified API for content delivery while integrating with best-of-breed services.

This guide focuses on multi-service architecture: how to combine Graph with commerce platforms, search engines, authentication services, and other APIs using patterns like Backend-for-Frontend (BFF), GraphQL Federation, and event-driven architecture. For single-framework integration, see Dynamic frontend integration.

What is composable architecture?

Composable architecture follows MACH principles:

  • Microservices – Small, independent services with single responsibilities
  • API-first – All functionality exposed through APIs
  • Cloud-native – Built for cloud infrastructure, scalable and resilient
  • Headless – Decoupled frontend and backend

Benefits:

  • Flexibility – Swap components without rebuilding the entire system
  • Best-of-breed – Choose the best tool for each function
  • Scalability – Scale individual services independently
  • Speed – Teams work on components in parallel
  • Future-proof – Evolve architecture incrementally

Graph in composable systems

Optimizely Graph fits into composable architecture as the content layer:

┌─────────────────────────────────────────────────────────────┐
│                    Presentation Layer                       │
│   (Next.js, React, Vue, Mobile Apps, Kiosks, Voice, etc.)   │
└─────────────────────────────────────────────────────────────┘
                            ↓ GraphQL API
┌─────────────────────────────────────────────────────────────┐
│                  Experience Orchestration                   │
│              (BFF, API Gateway, GraphQL Mesh)               │
└─────────────────────────────────────────────────────────────┘
          ↓              ↓              ↓              ↓
┌────────────────┬────────────────┬────────────────┬──────────┐
│  Content Layer │ Commerce Layer │  Search Layer  │   Auth   │
│ Optimizely     │   Shopify /    │   Algolia /    │  Auth0 / │
│     Graph      │  Commercetools │   Elastic      │  Okta    │
└────────────────┴────────────────┴────────────────┴──────────┘

Core patterns

Pattern 1: Backend for Frontend (BFF)

A BFF aggregates data from multiple services (Graph, commerce, search) into a unified API:

// pages/api/product/[id].js - BFF endpoint
export default async function handler(req, res) {
  const { id, locale = 'en' } = req.query;
  
  // Parallel requests to multiple services
  const [contentData, commerceData, reviewData] = await Promise.all([
    // Get content from Graph
    fetch(process.env.GRAPH_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
      },
      body: JSON.stringify({
        query: `
          query GetProductContent($key: String!, $locale: [Locales]) {
            Product(
              where: { _metadata: { key: { eq: $key } } }
              locale: $locale
            ) {
              item {
                description
                features
                images {
                  urlSmall
                  urlMedium
                  urlLarge
                }
              }
            }
          }
        `,
        variables: { key: id, locale: [locale] }
      })
    }).then(r => r.json()),
    
    // Get pricing/inventory from commerce platform
    fetch(`${process.env.COMMERCE_API}/products/${id}`, {
      headers: { 'Authorization': `Bearer ${process.env.COMMERCE_TOKEN}` }
    }).then(r => r.json()),
    
    // Get reviews from review service
    fetch(`${process.env.REVIEWS_API}/products/${id}/reviews`)
      .then(r => r.json())
  ]);
  
  // Combine data from multiple sources
  const product = {
    id,
    ...contentData.data.Product.item,
    price: commerceData.price,
    inventory: commerceData.inventory,
    reviews: reviewData
  };
  
  res.status(200).json(product);
}

Frontend consumption: See Dynamic frontend integration for React/Vue/Angular examples.

Pattern 2: GraphQL Federation

Stitch multiple GraphQL APIs into a unified schema using Apollo Federation:

// services/content-service.js
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/federation/v2.0")
  
  type Product @key(fields: "id") {
    id: ID!
    description: String
    images: [Image]
  }
  
  type Image {
    urlSmall: String
    urlMedium: String
    urlLarge: String
    alt: String
  }
`;

const resolvers = {
  Product: {
    __resolveReference: async (reference) => {
      // Query Optimizely Graph
      const response = await fetch(process.env.GRAPH_ENDPOINT, {
        method: 'POST',
        headers: {
          'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
        },
        body: JSON.stringify({
          query: `query GetProduct($key: String!, $locale: [Locales]) {
            Product(
              where: { _metadata: { key: { eq: $key } } }
              locale: $locale
            ) {
              item {
                description
                images {
                  urlSmall
                  urlMedium
                  urlLarge
                  alt
                }
              }
            }
          }`,
          variables: { key: reference.id, locale: [reference.locale ?? 'en'] }
        })
      });
      
      const { data } = await response.json();
      return data.Product.item;
    }
  }
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });

Commerce service:

// services/commerce-service.js
const typeDefs = gql`
  type Product @key(fields: "id") {
    id: ID!
    price: Float
    inventory: Int
  }
`;

const resolvers = {
  Product: {
    __resolveReference: async (reference) => {
      // Query commerce platform
      const response = await fetch(`${process.env.COMMERCE_API}/products/${reference.id}`);
      return response.json();
    }
  }
};

export const schema = buildSubgraphSchema({ typeDefs, resolvers });

Gateway:

// gateway.js
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'content', url: 'http://localhost:4001' },
    { name: 'commerce', url: 'http://localhost:4002' }
  ]
});

const server = new ApolloServer({ gateway });

Unified query:

query GetProduct($id: ID!) {
  product(id: $id) {
    # From content service (Graph)
    description
    images {
      urlSmall
      urlMedium
      urlLarge
    }
    
    # From commerce service
    price
    inventory
  }
}

Pattern 3: Event-driven architecture

Use webhooks to keep services synchronized.
Configure webhooks in Graph, and then handle the webhook events as they are received.

// api/webhooks/graph-updated.js
export default async function handler(req, res) {
  const { contentId, contentType, action } = req.body;
  
  // Verify webhook signature
  if (!verifyWebhookSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Trigger dependent updates
  await Promise.all([
    // Invalidate CDN cache
    invalidateCDN(contentId),
    
    // Update search index
    updateSearchIndex(contentId, contentType),
    
    // Notify connected clients via WebSocket
    notifyClients({ type: 'CONTENT_UPDATED', id: contentId }),
    
    // Trigger personalization engine recalculation
    recalculatePersonalization(contentId)
  ]);
  
  res.status(200).json({ success: true });
}

Pattern 4: Content as a Service (CaaS)

Expose Graph as a shared content service across multiple applications.
The content delivery service modifies the content for each platform.

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Website     │    │  Mobile App  │    │    Kiosk     │
│  (Next.js)   │    │(React Native)│    │   (React)    │
└──────────────┘    └──────────────┘    └──────────────┘
       ↓                    ↓                    ↓
┌─────────────────────────────────────────────────────┐
│          Content Delivery Service (BFF)             │
│  - Authentication & Authorization                   │
│  - Content transformation per platform              │
│  - Caching & performance optimization               │
└─────────────────────────────────────────────────────┘
                         ↓
              ┌──────────────────┐
              │ Optimizely Graph │
              │  (Content Layer) │
              └──────────────────┘

Content service implementation:

// services/content-service.js
export class ContentService {
  constructor(graphEndpoint, graphSingleKey) {
    this.endpoint = graphEndpoint;
    this.singleKey = graphSingleKey;
  }
  
  async getContent(contentType, filters, platform) {
    const query = this.buildQuery(contentType, filters, platform);
    
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `epi-single ${this.singleKey}`
      },
      body: JSON.stringify({ query })
    });
    
    const { data } = await response.json();
    
    // Transform for platform
    return this.transformForPlatform(data, platform);
  }
  
  buildQuery(contentType, filters, platform) {
    const fields = this.getFieldsForPlatform(platform);
    
    return `
      query Get${contentType}($filters: FilterInput!) {
        ${contentType}(where: $filters) {
          items {
            ${fields.join('\n')}
          }
        }
      }
    `;
  }
  
  getFieldsForPlatform(platform) {
    const baseFields = ['id', 'title'];
    
    const platformFields = {
      web: ['description', 'fullContent', 'images { urlSmall urlMedium }'],
      mobile: ['summary', 'thumbnailImage { urlSmall }'],
      kiosk: ['description', 'images { urlLarge }']
    };
    
    return [...baseFields, ...platformFields[platform]];
  }
  
  transformForPlatform(data, platform) {
    // Platform-specific transformations
    if (platform === 'mobile') {
      return data.map(item => ({
        ...item,
        // Compress for mobile
        content: this.truncate(item.content, 500)
      }));
    }
    
    return data;
  }
}

Composable commerce example

Combine Graph for content with a commerce platform for transactions.

Architecture:

  • Optimizely Graph – Product descriptions, images, features, marketing content
  • Commerce platform (Shopify, Commercetools) – Pricing, inventory, cart, checkout
  • Recommendation engine – Personalized product suggestions
  • Frontend – Aggregates data from all services

For framework integration examples, see Dynamic frontend integration.

Integration patterns

REST API wrapper

Expose Graph data through REST endpoints for legacy systems:

// pages/api/rest/articles.js
export default async function handler(req, res) {
  const { category, limit = 10, page = 1 } = req.query;
  
  const skip = (page - 1) * limit;
  
  const response = await fetch(process.env.GRAPH_ENDPOINT, {
    method: 'POST',
    headers: {
      'Authorization': `epi-single ${process.env.GRAPH_SINGLE_KEY}`
    },
    body: JSON.stringify({
      query: `
        query GetArticles($category: String, $limit: Int!, $skip: Int!) {
          Article(
            where: { category: { eq: $category } }
            limit: $limit
            skip: $skip
          ) {
            items { id, title, excerpt, publishedDate }
            total
          }
        }
      `,
      variables: { category, limit: parseInt(limit), skip }
    })
  });
  
  const { data } = await response.json();
  
  // Return REST-style response
  res.status(200).json({
    data: data.Article.items,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total: data.Article.total,
      totalPages: Math.ceil(data.Article.total / limit)
    }
  });
}

Content mesh

Create a content mesh connecting multiple content sources:

// lib/content-mesh.js
export class ContentMesh {
  constructor(sources) {
    this.sources = sources; // { graph, cms, dam, etc. }
  }
  
  async getUnifiedContent(contentId, locale = 'en') {
    const source = this.determineSource(contentId);
    
    switch (source) {
      case 'graph':
        return this.fetchFromGraph(contentId, locale);
      case 'dam':
        return this.fetchFromDAM(contentId);
      default:
        throw new Error(`Unknown source: ${source}`);
    }
  }
  
  async fetchFromGraph(contentId, locale) {
    const response = await fetch(this.sources.graph.endpoint, {
      method: 'POST',
      headers: {
        'Authorization': `epi-single ${this.sources.graph.singleKey}`
      },
      body: JSON.stringify({
        query: `query GetContent($key: String!, $locale: [Locales]) { Content(where: { _metadata: { key: { eq: $key } } }, locale: $locale) { item { ... } } }`,
        variables: { key: contentId, locale: [locale] }
      })
    });
    
    const { data } = await response.json();
    return this.normalizeContent(data.Content.item, 'graph');
  }
  
  normalizeContent(content, source) {
    // Transform to unified format
    return {
      id: content.id,
      title: content.title || content.name,
      body: content.content || content.description,
      source
    };
  }
}

Best practices

1. Design for failure

Services will fail. Build resilience:

async function fetchWithFallback(primary, fallback) {
  try {
    const response = await fetch(primary);
    if (!response.ok) throw new Error('Primary failed');
    return response.json();
  } catch (error) {
    console.error('Primary service failed, using fallback:', error);
    return fetch(fallback).then(r => r.json());
  }
}

The fallback URL can point to a Redis cache endpoint, a CDN-served static snapshot, or a secondary service.

2. Implement circuit breakers

Prevent cascading failures:

import CircuitBreaker from 'opossum';

const options = {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000
};

const breaker = new CircuitBreaker(fetchFromGraph, options);

breaker.fallback(() => getCachedData());

breaker.on('open', () => console.log('Circuit opened!'));

3. Use caching strategically

Cache at multiple levels:

// CDN caching
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');

// Application caching
const cachedData = await redis.get(cacheKey);
if (cachedData) return JSON.parse(cachedData);

// Query result caching in Apollo
const { data } = useQuery(GET_CONTENT, {
  fetchPolicy: 'cache-first',
  nextFetchPolicy: 'cache-and-network'
});

4. Monitor service dependencies

Track service health:

// Health check endpoint
app.get('/health', async (req, res) => {
  const services = await Promise.allSettled([
    checkGraph(),
    checkCommerce(),
    checkSearch()
  ]);
  
  const health = {
    status: services.every(s => s.status === 'fulfilled') ? 'healthy' : 'degraded',
    services: {
      graph: services[0].status === 'fulfilled',
      commerce: services[1].status === 'fulfilled',
      search: services[2].status === 'fulfilled'
    }
  };
  
  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});

5. Version your APIs

Enable independent evolution:

// v1/api/content.js
export default function handlerV1(req, res) {
  // Legacy format
}

// v2/api/content.js
export default function handlerV2(req, res) {
  // New format with breaking changes
}

Migration strategy

Strangler fig pattern

Gradually replace the monolith with composable services:

Phase 1: Monolith handles all requests
┌──────────────────────────────────────┐
│          Monolithic CMS              │
└──────────────────────────────────────┘

Phase 2: Route some content through Graph
┌──────────────────────────────────────┐
│  Proxy Layer (decides routing)       │
└──────────────────────────────────────┘
      ↓                    ↓
┌──────────┐      ┌─────────────────┐
│ Monolith │      │ Optimizely Graph│
└──────────┘      └─────────────────┘

Phase 3: All content through composable stack
┌──────────────────────────────────────┐
│        Modern Frontend               │
└──────────────────────────────────────┘
               ↓
┌──────────────────────────────────────┐
│     Optimizely Graph + Services      │
└──────────────────────────────────────┘

Next steps


Did this page help you?