Introduction
Composable architecture treats every business capability (catalog, cart, checkout, search, CMS, personalization) as an independent, replaceable component that communicates through well-defined APIs. Unlike monolithic suites where upgrading the CMS requires touching every other system, composable systems let teams swap, upgrade, or add components without cascading dependencies.
The MACH Alliance defines the technical foundation: Microservices, API-first, Cloud-native SaaS, and Headless. This guide provides concrete implementation patterns: a GraphQL federation layer that composes multiple backend APIs into a single endpoint, Kubernetes deployment of composed services with service mesh, and a complete e-commerce example demonstrating how catalog, cart, and checkout services compose together.
Architecture Overview
flowchart TD
subgraph Frontends["Headless Frontends"]
Web[Web App<br/>Next.js]
Mobile[Mobile App<br/>React Native]
POS[POS Terminal]
end
subgraph Composition["API Composition Layer"]
GQL[GraphQL Federation<br/>Apollo Router]
end
subgraph Services["Composable Services"]
Catalog[Catalog Service<br/>commercetools / Custom]
Cart[Cart Service<br/>Redis-backed]
Checkout[Checkout Service<br/>PCI-compliant]
CMS[CMS / Content<br/>Contentful / Strapi]
Search[Search Service<br/>Algolia / Meilisearch]
Personalize[Personalization<br/>Dynamic Yield / Custom]
end
subgraph Infrastructure["Cloud-Native Infrastructure"]
K8s[Kubernetes]
Mesh[Service Mesh<br/>Istio Ambient]
Obs[Observability<br/>Prometheus + Grafana]
end
Web --> GQL
Mobile --> GQL
POS --> GQL
GQL --> Catalog
GQL --> Cart
GQL --> Checkout
GQL --> CMS
GQL --> Search
GQL --> Personalize
Services --> K8s
K8s --> Mesh
K8s --> Obs
API Composition with GraphQL Federation
GraphQL Federation lets you compose multiple backend GraphQL services into a single unified graph. Each service owns its domain types and extends types from other services:
Catalog Service (Products)
# products.graphql — owned by catalog service
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
currency: String!
category: String
inStock: Boolean
}
extend type Query {
products(category: String): [Product!]!
product(id: ID!): Product
}
Cart Service (extends Product type)
# cart.graphql — cart service extends Product with cart-specific data
type CartItem @key(fields: "id") {
id: ID!
productId: ID!
quantity: Int!
}
extend type Product @key(fields: "id") {
id: ID! @external
cartQuantity: Int @requires(fields: "id")
}
extend type Query {
cart(userId: ID!): [CartItem!]!
}
Apollo Router Configuration
# router.yaml — Apollo Federation Router
supergraph:
listen: 0.0.0.0:4000
cors:
origins:
- https://www.example.com
- https://admin.example.com
rhai:
scripts: ./rhai-scripts
main: main.rhai
headers:
all:
request:
- propagate:
named: "authorization"
# Rate limiting per subgraph
subgraphs:
products:
routing_url: http://catalog-service:4001/graphql
cart:
routing_url: http://cart-service:4002/graphql
checkout:
routing_url: http://checkout-service:4003/graphql
Example: E-Commerce Checkout Flow
A checkout transaction spans three services: cart (read items), catalog (get prices), and checkout (process payment). The frontend makes a single GraphQL query:
# Frontend calls the federation router with one query
mutation Checkout {
checkout {
createOrder(input: { paymentMethod: "card" }) {
orderId
total
items {
product { name price }
quantity
}
status
}
}
}
The router resolves this by calling cart → catalog → checkout in sequence, but the frontend sees only one round trip.
Kubernetes Deployment
Each composable service is an independently deployable Kubernetes workload:
# catalog-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
labels:
app: catalog
service: composable-commerce
spec:
replicas: 2
selector:
matchLabels:
app: catalog
template:
metadata:
labels:
app: catalog
spec:
containers:
- name: catalog
image: registry.example.com/catalog:v1.24.0
ports:
- containerPort: 4001
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: catalog-db
key: url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4001
---
apiVersion: v1
kind: Service
metadata:
name: catalog-service
spec:
selector:
app: catalog
ports:
- port: 4001
targetPort: 4001
CI/CD Pipeline (GitHub Actions)
# .github/workflows/deploy-service.yml
name: Deploy Composable Service
on:
push:
paths:
- 'services/catalog/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
docker build -t registry.example.com/catalog:${{ github.sha }} ./services/catalog
docker push registry.example.com/catalog:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/catalog-service \
catalog=registry.example.com/catalog:${{ github.sha }}
kubectl rollout status deployment/catalog-service
Each service is deployed independently. A cart update does not require redeploying the catalog or checkout services.
Composable vs Monolithic Decision Framework
| Factor | Monolithic Suite | Composable (MACH) |
|---|---|---|
| Deployment | One deploy affects everything | Independent per service |
| Upgrade risk | Full regression test needed | Test only changed service |
| Vendor lock-in | Single vendor ecosystem | Best-of-breed per capability |
| Time to market | Slower (coordinated releases) | Faster (independent teams) |
| Operational complexity | Lower (single system) | Higher (multiple services) |
| Best for | Small teams, simple requirements | Large teams, complex requirements |
Event-Driven Communication Between Services
While GraphQL federation handles synchronous API composition, many workflows benefit from asynchronous event-driven communication:
# Event schema for inter-service communication
from pydantic import BaseModel
from datetime import datetime
from enum import Enum
from typing import Any, Dict
class EventType(str, Enum):
ORDER_CREATED = "order.created"
ORDER_CANCELLED = "order.cancelled"
INVENTORY_UPDATED = "inventory.updated"
PAYMENT_PROCESSED = "payment.processed"
SHIPMENT_CREATED = "shipment.created"
class DomainEvent(BaseModel):
event_id: str
event_type: EventType
source_service: str
timestamp: datetime
data: Dict[str, Any]
correlation_id: str # Track across services
# Cart service publishes event
async def on_order_created(order_id: str, items: list):
event = DomainEvent(
event_id=str(uuid.uuid4()),
event_type=EventType.ORDER_CREATED,
source_service="cart",
timestamp=datetime.utcnow(),
data={"order_id": order_id, "items": items},
correlation_id=order_id
)
await event_bus.publish("commerce.orders", event.model_dump_json())
# Inventory service subscribes
async def handle_order_created(message: str):
event = DomainEvent.model_validate_json(message)
for item in event.data["items"]:
await inventory_service.reserve(item["product_id"], item["quantity"])
await event_bus.publish("commerce.inventory", DomainEvent(
event_id=str(uuid.uuid4()),
event_type=EventType.INVENTORY_UPDATED,
source_service="inventory",
timestamp=datetime.utcnow(),
data={"correlation_id": event.correlation_id},
correlation_id=event.correlation_id
).model_dump_json())
Service Mesh Integration with Istio Ambient Mesh
Service mesh provides networking, security, and observability for composable services without modifying application code:
# Istio Ambient mesh configuration for composable services
apiVersion: v1
kind: ServiceAccount
metadata:
name: catalog-service
---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: catalog-mtls
spec:
selector:
matchLabels:
app: catalog
mtls:
mode: STRICT # All traffic encrypted
---
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
name: catalog-tracing
spec:
selector:
matchLabels:
app: catalog
tracing:
- providers:
- name: otel-tracer
randomSamplingPercentage: 1.0
---
# Traffic management between composable services
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: catalog-routing
spec:
hosts:
- catalog-service
http:
- match:
- headers:
x-deployment-preview:
exact: "true"
route:
- destination:
host: catalog-service-preview
timeout: 5s
- route:
- destination:
host: catalog-service
weight: 99
- destination:
host: catalog-service-preview
weight: 1
retries:
attempts: 3
perTryTimeout: 2s
timeout: 10s
Error Handling and Resilience Patterns
In a composable system, services must handle failures gracefully using circuit breakers, fallbacks, and timeouts:
import asyncio
from circuitbreaker import circuit
from typing import TypeVar, Callable, Awaitable
T = TypeVar("T")
class ComposableServiceClient:
"""Client with built-in resilience patterns."""
def __init__(self, service_name: str, base_url: str, timeout: float = 5.0):
self.service_name = service_name
self.base_url = base_url
self.timeout = timeout
self.fallback_cache = {}
@circuit(failure_threshold=5, recovery_timeout=30)
async def call(self, endpoint: str, **kwargs) -> dict:
"""Circuit-breaker protected service call."""
async with asyncio.timeout(self.timeout):
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.base_url}{endpoint}",
headers={"X-Correlation-Id": kwargs.get("correlation_id", "")},
**kwargs
) as response:
if response.status >= 500:
raise ServiceError(f"{self.service_name} returned {response.status}")
return await response.json()
async def call_with_fallback(
self, endpoint: str, fallback_key: str, **kwargs
) -> dict:
"""Call service with cached fallback on failure."""
try:
result = await self.call(endpoint, **kwargs)
self.fallback_cache[fallback_key] = result # Update cache on success
return result
except (ServiceError, asyncio.TimeoutError) as e:
# Return cached data if available
if fallback_key in self.fallback_cache:
return {
"data": self.fallback_cache[fallback_key],
"source": "cache",
"error": str(e)
}
raise
# Usage
catalog_client = ComposableServiceClient("catalog", "http://catalog-service:4001")
async def get_product_details(product_id: str) -> dict:
return await catalog_client.call_with_fallback(
f"/products/{product_id}",
fallback_key=f"product_{product_id}"
)
Caching and Performance Optimization
Composable architectures benefit from multi-layer caching at API gateway, service, and data levels:
import aiocache
from aiocache import cached
from aiocache.serializers import JsonSerializer
class CatalogServiceCache:
"""Multi-layer caching for composable catalog service."""
@cached(
ttl=300, # 5 minutes
key_builder=lambda fn, *args, **kwargs: f"product:{args[0]}",
serializer=JsonSerializer(),
namespace="catalog"
)
async def get_product(self, product_id: str) -> dict:
"""Cache product data with Redis backend."""
return await self.db.fetch_product(product_id)
@cached(
ttl=60, # 1 minute for frequently changing inventory
key_builder=lambda fn, *args: f"inventory:{args[0]}",
serializer=JsonSerializer(),
namespace="inventory"
)
async def get_inventory(self, product_id: str) -> dict:
return await self.inventory_client.check_stock(product_id)
# Invalidate cache when product updates
async def update_product(self, product_id: str, data: dict):
await self.db.update_product(product_id, data)
await aiocache.Cache().delete(f"catalog:product:{product_id}")
# GraphQL DataLoader prevents N+1 queries
from graphql import GraphQLResolveInfo
from aiodataloader import DataLoader
class ProductLoader(DataLoader):
async def batch_load(self, keys: list) -> list:
products = await self.db.fetch_products_by_ids(keys)
product_map = {p["id"]: p for p in products}
return [product_map.get(k) for k in keys]
product_loader = ProductLoader()
# In resolver
async def resolve_product(parent, info: GraphQLResolveInfo, id: str):
return await product_loader.load(id)
Testing Composable Systems
Testing a composable system requires strategies at multiple levels:
Contract Testing with Pact
import pact
# Consumer-side test (frontend team)
@pytest.fixture
def consumer():
return pact.Consumer("web-frontend")
@pytest.fixture
def provider():
return pact.Provider("catalog-service")
def test_get_product(consumer, provider):
(consumer
.upon_receiving("a request for product 123")
.with_request("GET", "/products/123")
.will_respond_with(200, body={
"id": "123",
"name": "Widget",
"price": 29.99
}))
with provider:
result = await catalog_client.get_product("123")
assert result["name"] == "Widget"
Integration Test with Test Containers
import pytest
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
@pytest.mark.asyncio
async def test_catalog_integration():
with PostgresContainer("postgres:16") as pg:
with RedisContainer("redis:7") as redis:
# Wire up real dependencies
db_url = pg.get_connection_url()
redis_url = redis.get_connection_url()
# Start the catalog service with real DB and cache
app = create_catalog_service(db_url=db_url, redis_url=redis_url)
# Run tests against real service
result = await app.get_product("123")
assert result is not None
Migration Strategy: Monolith to Composable
Most organizations start with a monolith and decompose incrementally:
| Phase | Actions | Risk | Duration |
|---|---|---|---|
| 1. Strangler Fig | Route specific endpoints to new microservice | Low | 2-4 weeks |
| 2. Cart Service | Extract cart into independent service | Medium | 4-8 weeks |
| 3. Catalog Service | Extract product catalog with GraphQL | Medium | 4-8 weeks |
| 4. Checkout Service | Extract checkout with event-driven patterns | High | 8-12 weeks |
| 5. Search Service | Replace internal search with dedicated service | Low | 2-4 weeks |
Use the strangler fig pattern: route traffic for specific paths to the new composable service while the monolith handles everything else. Incrementally increase the routed traffic as confidence grows.
# API Gateway strangler fig routing
routes = {
"/api/checkout": {"service": "checkout-service", "weight": 10}, # Start with 10%
"/api/catalog": {"service": "catalog-service", "weight": 50}, # Half migrated
"/api/orders": {"service": "order-service", "weight": 100}, # Fully migrated
"/api/*": {"service": "monolith", "weight": 100} # Everything else
}
Resources
- Apollo Federation Documentation — GraphQL API composition
- MACH Alliance — Microservices, API-first, Cloud-native, Headless standards
- Kubernetes Service Mesh (Istio) — Composable service networking
- commercetools Composable Commerce — Headless commerce platform
- Contentful Headless CMS — API-first content management
Comments