
Composable Commerce
- 58 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Architect a store using MACH principles - microservices, API-first, cloud-native, headless - so each commerce capability is a best-of-breed swappable service.
About
A skill for designing composable commerce on MACH principles with event-driven service integration and headless frontends. A developer uses it to replace a monolithic platform with independently upgradeable services.
- MACH architecture with best-of-breed services (commercetools, Contentful)
- Event-driven coordination and composable-stack operations
Composable Commerce by the numbers
- 58 all-time installs (skills.sh)
- Ranked #3,178 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill composable-commerceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Architect a store using MACH principles - microservices, API-first, cloud-native, headless - so each commerce capability is a best-of-breed swappable service.
Files
Composable Commerce
Overview
Composable commerce is an architectural approach based on MACH principles — Microservices, API-first, Cloud-native, Headless — where each commerce capability (cart, catalog, search, CMS, checkout, loyalty) is provided by a best-of-breed service rather than a monolithic platform. Services communicate via APIs and events, enabling teams to replace or upgrade individual capabilities without touching the rest of the system. This skill covers MACH architecture patterns, service integration, event-driven coordination, and the operational considerations of running a composable stack in production.
When to Use This Skill
- When a monolithic platform (Magento, Salesforce CC) can no longer scale with your team or traffic patterns
- When different domains (catalog, checkout, loyalty) have divergent release cadences and ownership
- When you need to mix best-of-breed vendors — e.g., Algolia for search, Contentful for CMS, commercetools for commerce
- When entering new markets requiring different fulfillment, pricing, or tax providers per region
- When building a platform that must support multiple storefronts (web, mobile, in-store, B2B portal) from a single backend
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services. WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress. Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- PostgreSQL (or your preferred relational database)
- A search service (Algolia, Elasticsearch, or Typesense)
- Stripe account and API keys
- An email sending service (SendGrid, AWS SES, or Postmark)
Core Instructions
1. Design service boundaries around commerce capabilities
A typical composable commerce stack separates capabilities into discrete services:
| Capability | Example Services |
|---|---|
| Product Catalog | commercetools, Akeneo PIM, Salsify |
| Search & Discovery | Algolia, Elasticsearch, Constructor.io |
| CMS / Content | Contentful, Sanity, Storyblok |
| Cart & Checkout | commercetools, Elastic Path, Medusa |
| Payments | Stripe, Adyen, Braintree |
| Tax | Avalara, TaxJar |
| Shipping & Fulfillment | EasyPost, ShipBob, custom OMS |
| Customer Identity | Auth0, Okta, Cognito |
| Loyalty & Promotions | Talon.One, Voucherify |
| Email / Notifications | SendGrid, Customer.io |
Define bounded contexts: each service owns its data and exposes it only via APIs. Never share databases between services.
2. Implement an API composition layer (BFF)
A Backend-for-Frontend (BFF) aggregates multiple upstream APIs into a single request, tailored to what the frontend needs:
// bff/src/routes/product-page.ts
import {getProduct} from '../services/catalog';
import {getSearchReviews} from '../services/reviews';
import {getRecommendations} from '../services/recommendations';
import {getInventory} from '../services/inventory';
export async function productPageData(productId: string, customerId?: string) {
// Parallel fetch from independent services
const [product, inventory, recommendations] = await Promise.all([
getProduct(productId),
getInventory(productId),
getRecommendations(productId, customerId),
]);
// Sequential: reviews needs product.sku
const reviews = await getSearchReviews(product.sku);
return {
product,
inventory,
recommendations,
reviews,
};
}Expose the BFF as a GraphQL API using schema stitching or federation:
import {buildHTTPExecutor} from '@graphql-tools/executor-http';
import {stitchSchemas} from '@graphql-tools/stitch';
const gatewaySchema = await stitchSchemas({
subschemas: [
{ schema: await introspectSchema(catalogExecutor), executor: catalogExecutor },
{ schema: await introspectSchema(inventoryExecutor), executor: inventoryExecutor },
{ schema: await introspectSchema(reviewsExecutor), executor: reviewsExecutor },
],
});3. Use event-driven architecture for cross-service coordination
Services should communicate asynchronously for workflows that span multiple capabilities (order placed → inventory reserved → fulfillment triggered → email sent):
// Order service publishes an event after checkout
import {EventBridge} from '@aws-sdk/client-eventbridge';
const eventBridge = new EventBridge({region: 'us-east-1'});
async function publishOrderPlaced(order: Order) {
await eventBridge.putEvents({
Entries: [
{
Source: 'commerce.orders',
DetailType: 'OrderPlaced',
Detail: JSON.stringify({
orderId: order.id,
customerId: order.customerId,
lineItems: order.lineItems,
totalAmount: order.totalAmount,
currency: order.currency,
}),
EventBusName: 'commerce-events',
},
],
});
}
// Inventory service subscribes and reserves stock
// EventBridge rule routes OrderPlaced → Lambda → inventory-service
export async function handler(event: EventBridgeEvent<'OrderPlaced', OrderPayload>) {
const {orderId, lineItems} = event.detail;
await reserveInventory(lineItems);
await publishInventoryReserved(orderId);
}4. Implement the Saga pattern for distributed transactions
When a multi-step workflow must be atomic across services, use orchestrated sagas with compensating transactions:
// Choreography-based saga for order fulfillment
// Each service publishes events and reacts to others
// Order Service
on('CheckoutCompleted', async ({orderId, paymentIntentId}) => {
await orders.create({orderId, status: 'pending_payment'});
await publish('OrderCreated', {orderId, paymentIntentId});
});
// Payment Service
on('OrderCreated', async ({orderId, paymentIntentId}) => {
try {
await capturePayment(paymentIntentId);
await publish('PaymentCaptured', {orderId});
} catch (err) {
await publish('PaymentFailed', {orderId, reason: err.message});
}
});
// Fulfillment Service
on('PaymentCaptured', async ({orderId}) => {
await fulfillment.schedule(orderId);
});
// Order Service — compensate on failure
on('PaymentFailed', async ({orderId}) => {
await orders.update(orderId, {status: 'payment_failed'});
await releaseInventory(orderId);
await notifyCustomer(orderId, 'payment_failed');
});5. Manage API versioning and backward compatibility
In a composable stack, services evolve independently. Use additive versioning strategies:
// Use content negotiation or URL versioning
// GET /api/v2/products/:id
// Accept: application/vnd.commerce.product.v2+json
// Apply the Tolerant Reader pattern — ignore unknown fields
interface ProductV1 {
id: string;
name: string;
price: number;
}
// V2 adds fields — existing consumers still work
interface ProductV2 extends ProductV1 {
categories?: string[];
attributes?: Record<string, string>;
brand?: string;
}
// Use feature flags to roll out breaking changes
async function getProduct(id: string, apiVersion: '1' | '2' = '1') {
const product = await catalog.findById(id);
return apiVersion === '2' ? toProductV2(product) : toProductV1(product);
}6. Implement circuit breakers for resilience
When one service degrades, prevent cascading failures across the entire stack:
import CircuitBreaker from 'opossum';
const inventoryCircuit = new CircuitBreaker(checkInventory, {
timeout: 3000, // Request timeout in ms
errorThresholdPercentage: 50, // Open circuit if 50% of requests fail
resetTimeout: 30000, // Try again after 30 seconds
});
inventoryCircuit.fallback(() => ({
available: true, // Optimistic fallback — show as available
quantity: null, // Don't show exact quantity
}));
inventoryCircuit.on('open', () => {
logger.warn('Inventory service circuit OPEN — using fallback');
metrics.increment('circuit_breaker.inventory.open');
});
// Usage
const stock = await inventoryCircuit.fire(productId);Examples
commercetools SDK integration for catalog
import {createApiBuilderFromCtpClient} from '@commercetools/platform-sdk';
import {ClientBuilder} from '@commercetools/sdk-client-v2';
const ctpClient = new ClientBuilder()
.withProjectKey(process.env.CTP_PROJECT_KEY!)
.withClientCredentialsFlow({
host: 'https://auth.us-central1.gcp.commercetools.com',
projectKey: process.env.CTP_PROJECT_KEY!,
credentials: {
clientId: process.env.CTP_CLIENT_ID!,
clientSecret: process.env.CTP_CLIENT_SECRET!,
},
scopes: ['manage_project:' + process.env.CTP_PROJECT_KEY],
fetch,
})
.withHttpMiddleware({host: 'https://api.us-central1.gcp.commercetools.com', fetch})
.build();
const apiRoot = createApiBuilderFromCtpClient(ctpClient).withProjectKey({
projectKey: process.env.CTP_PROJECT_KEY!,
});
// Fetch product by slug
const product = await apiRoot
.products()
.get({queryArgs: {where: `slug(en="${slug}")`, expand: ['productType']}})
.execute();Algolia search with Contentful CMS enrichment
import algoliasearch from 'algoliasearch';
import {createClient as createContentfulClient} from 'contentful';
const algolia = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_SEARCH_KEY!);
const contentful = createContentfulClient({space: process.env.CONTENTFUL_SPACE_ID!, accessToken: process.env.CONTENTFUL_TOKEN!});
async function searchProducts(query: string, filters?: string) {
// Search in Algolia for product IDs and structured data
const {hits} = await algolia.initIndex('products').search<AlgoliaProduct>(query, {
filters,
attributesToRetrieve: ['objectID', 'name', 'price', 'contentfulEntryId'],
});
// Enrich with rich content from Contentful
const contentEntryIds = hits.map(h => h.contentfulEntryId).filter(Boolean);
const contentEntries = await contentful.getEntries({
content_type: 'productPage',
'sys.id[in]': contentEntryIds.join(','),
});
const contentMap = new Map(contentEntries.items.map(e => [e.sys.id, e]));
return hits.map(hit => ({
...hit,
content: contentMap.get(hit.contentfulEntryId),
}));
}Best Practices
- Define a clear data ownership model — each piece of data has exactly one system of record; other services read from it via API, never write to it directly
- Design for eventual consistency — cross-service data will be temporarily inconsistent after an event; build UIs and workflows that tolerate this (e.g., optimistic inventory, compensating transactions)
- Use an API gateway for cross-cutting concerns — authentication, rate limiting, request logging, and SSL termination belong at the gateway, not in every service
- Implement distributed tracing — use OpenTelemetry with a trace ID propagated across all service calls; this is essential for debugging multi-service request failures
- Version your events — event schemas change over time; include a
schemaVersionfield in every event payload and maintain backward-compatible consumers - Use a service mesh for service-to-service traffic — tools like Istio or AWS App Mesh handle mutual TLS, load balancing, and retries at the infrastructure layer
- Start with a modular monolith — decompose into microservices only when you have clear team ownership boundaries and operational maturity; premature decomposition creates accidental complexity
Common Pitfalls
| Problem | Solution |
|---|---|
| Distributed transaction failures leave data inconsistent | Implement sagas with compensating transactions; use outbox pattern to guarantee event publication after DB write |
| Service latency compounds in the critical path | Move non-critical services off the critical path using async events; set aggressive timeouts and circuit breakers on all external calls |
| API contract breaks downstream consumers | Use consumer-driven contract testing (Pact) so breaking changes are caught before deployment |
| Shared database creates hidden coupling | Enforce the rule: one service, one schema; use event sourcing or change data capture for cross-service data propagation |
| Debugging failures across 10 services | Implement distributed tracing with OpenTelemetry from day one; correlate logs with a single trace ID per user request |
Related Skills
- @commerce-api-gateway
- @saleor-development
- @shopify-hydrogen
- @webhook-architecture
- @flash-sale-scaling
- @edge-commerce
{
"context": "Tests whether the agent applies composable commerce API versioning patterns (additive versioning, Tolerant Reader, URL versioning, feature flags) and implements distributed tracing using OpenTelemetry with trace ID propagation across service calls.",
"type": "weighted_checklist",
"checklist": [
{
"name": "URL versioning",
"max_score": 10,
"description": "Route handlers use URL versioning (e.g. /api/v1/products and /api/v2/products) or content negotiation headers to distinguish API versions"
},
{
"name": "V2 extends V1 additively",
"max_score": 12,
"description": "ProductV2 type extends ProductV1 (or includes all V1 fields) and only adds new optional fields — does NOT remove or change the type of any V1 field"
},
{
"name": "New V2 fields are optional",
"max_score": 10,
"description": "New fields added in V2 (such as categories, attributes, brand) are defined as optional (?) so consumers not sending them are unaffected"
},
{
"name": "Feature flag for breaking changes",
"max_score": 10,
"description": "Code includes a feature flag, API version parameter, or conditional check that controls whether V2 fields are returned, rather than always returning them"
},
{
"name": "OpenTelemetry import",
"max_score": 12,
"description": "Imports from '@opentelemetry/...' packages (NOT a custom trace implementation or a different tracing library like jaeger-client or dd-trace as the primary mechanism)"
},
{
"name": "Trace ID propagation",
"max_score": 12,
"description": "Tracing setup propagates a trace/span context to outbound HTTP calls so downstream services can be correlated (e.g. using a propagator or context injection into request headers)"
},
{
"name": "Single trace ID per request",
"max_score": 8,
"description": "Documentation, comments, or code demonstrates that all service calls for a single user request share one trace ID (not a new trace per service hop)"
},
{
"name": "Data ownership — one system of record",
"max_score": 10,
"description": "Code or architectural comments establish that the Product Service is the sole owner of product data — other services read via API and do not write directly to this service's data store"
},
{
"name": "No shared database pattern",
"max_score": 8,
"description": "Code or documentation does NOT show or suggest other services sharing the product service's database directly (no shared connection strings, no cross-service ORM models)"
},
{
"name": "Modular monolith recommendation",
"max_score": 8,
"description": "Code comments, README, or architectural notes recommend or acknowledge starting with a modular monolith before full microservice decomposition, or cite the risk of premature decomposition"
}
]
}
Evolving the Product Service API and Adding Observability
Problem Description
A composable commerce platform has a Product Service that has been running in production for 18 months. The current API (v1) exposes basic product data — id, name, and price. The product team wants to add richer attributes in a v2: product categories, custom attributes map, and brand. However, several existing consumers (a mobile app, a partner integration, and a legacy reporting tool) are still on the v1 contract and cannot be migrated immediately. Any change that breaks v1 consumers would cause an incident.
At the same time, the platform's SRE team has been struggling to diagnose production failures. When a customer complaint comes in about a bad checkout, they need to trace the request across the product service, cart service, and checkout service, but currently each service logs independently with no shared correlation. The SRE team wants all service calls for a single user request to be identifiable under one trace.
You have been brought in to update the Product Service codebase to address both concerns.
Output Specification
Produce the following files:
product-service/src/types.ts— TypeScript type definitions for ProductV1 and ProductV2product-service/src/routes/products.ts— Express (or similar) route handlers for the v1 and v2 product endpointsproduct-service/src/tracing.ts— OpenTelemetry setup and trace propagation helpersproduct-service/package.json— package manifest listing dependencies
The versioning implementation should ensure existing v1 consumers continue to work without modification. The tracing implementation should show how a trace ID is established and propagated so that downstream service calls can be correlated. Code does not need to connect to a live database but should be complete and correct TypeScript.
{
"context": "Tests whether the agent implements a BFF aggregation layer following composable commerce patterns: parallel fetching with Promise.all for independent services, sequential fetching for dependent calls, GraphQL gateway via schema stitching, and opossum circuit breakers with correct configuration and fallback for resilience.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Parallel fetch with Promise.all",
"max_score": 12,
"description": "Uses Promise.all (or equivalent) to fetch from at least two independent services simultaneously rather than awaiting them sequentially"
},
{
"name": "Sequential for dependent calls",
"max_score": 10,
"description": "Makes a sequential await (not inside Promise.all) for a service call that depends on the result of a prior call (e.g. reviews fetched after obtaining a product SKU/ID)"
},
{
"name": "GraphQL stitching package",
"max_score": 12,
"description": "Imports and uses @graphql-tools/stitch (specifically stitchSchemas) for the GraphQL gateway setup, NOT Apollo Federation or hand-rolled GraphQL schemas"
},
{
"name": "HTTP executor for subschemas",
"max_score": 8,
"description": "Uses @graphql-tools/executor-http (buildHTTPExecutor) to connect remote subschemas in the stitched gateway"
},
{
"name": "opossum package",
"max_score": 12,
"description": "Imports CircuitBreaker from the 'opossum' package (not a custom implementation or alternative library)"
},
{
"name": "Circuit breaker timeout config",
"max_score": 8,
"description": "Circuit breaker instance is configured with a numeric 'timeout' property (request timeout in milliseconds)"
},
{
"name": "Circuit breaker threshold config",
"max_score": 8,
"description": "Circuit breaker instance is configured with 'errorThresholdPercentage' property"
},
{
"name": "Circuit breaker reset config",
"max_score": 8,
"description": "Circuit breaker instance is configured with 'resetTimeout' property"
},
{
"name": "Fallback function",
"max_score": 12,
"description": "Calls .fallback() on the circuit breaker instance to provide a default response when the circuit is open"
},
{
"name": "Circuit open event handling",
"max_score": 10,
"description": "Subscribes to the 'open' event on the circuit breaker (e.g. .on('open', ...)) to emit a log or metric when the circuit opens"
}
]
}
Product Detail Page Backend Aggregation Service
Problem Description
A mid-sized fashion retailer is migrating from a monolithic Magento store to a composable commerce stack. Their product detail page currently requires data from four separate services: a product catalog (with full product attributes), an inventory service (real-time stock levels), a reviews service (customer reviews and ratings), and a recommendations engine (personalized product suggestions). Each service has its own independent API.
The frontend team is building a React storefront and is frustrated because making four separate API calls from the browser introduces waterfalls, exposes internal API structure, and makes it hard to handle partial failures gracefully. They need a single backend endpoint that aggregates all the data they need for a product page.
The inventory service in particular has a track record of intermittent slowdowns under load. The team has been burned before when inventory failures cascaded to take down the entire product page. They need the aggregation layer to degrade gracefully when inventory is unavailable — showing the product as available with no exact stock count — rather than returning an error to the user.
The team also wants this aggregation API to be queryable with GraphQL so that different frontends (web, mobile app) can request exactly the fields they need.
Output Specification
Implement the BFF service in TypeScript. Produce the following files:
bff/src/routes/product-page.ts— the aggregation function that fetches from catalog, inventory, reviews, and recommendationsbff/src/gateway.ts— the GraphQL gateway setup that exposes the aggregated databff/src/resilience/inventory-circuit.ts— the circuit breaker configuration for the inventory servicebff/package.json— package manifest listing dependencies
The code does not need to be runnable against live services, but should be complete and correct TypeScript demonstrating the patterns used.
{
"context": "Tests whether the agent implements a choreography-based saga for distributed order processing: using AWS EventBridge for async event publishing, including structured event fields (Source, DetailType, EventBusName), adding schemaVersion to event payloads, implementing compensating transactions on payment failure, and referencing the outbox pattern for reliable event delivery.",
"type": "weighted_checklist",
"checklist": [
{
"name": "EventBridge import",
"max_score": 10,
"description": "Imports EventBridge from '@aws-sdk/client-eventbridge' for publishing events (not SNS, SQS, or a custom in-memory bus as the primary mechanism)"
},
{
"name": "putEvents with Source field",
"max_score": 8,
"description": "Calls eventBridge.putEvents() with an entry that includes a 'Source' field (e.g. 'commerce.orders')"
},
{
"name": "putEvents with DetailType field",
"max_score": 8,
"description": "Event entries include a 'DetailType' field naming the event (e.g. 'OrderPlaced', 'PaymentCaptured')"
},
{
"name": "EventBusName field",
"max_score": 8,
"description": "Event entries include an 'EventBusName' field specifying the target event bus"
},
{
"name": "Choreography-based saga",
"max_score": 10,
"description": "Each service has its own event handlers that subscribe to events and publish follow-on events — NOT a single orchestrator that calls each service directly"
},
{
"name": "Payment failure compensation — inventory release",
"max_score": 10,
"description": "On payment failure event, inventory that was reserved is explicitly released (calls a function like releaseInventory or equivalent)"
},
{
"name": "Payment failure compensation — order status update",
"max_score": 8,
"description": "On payment failure event, the order's status is updated to a failed/cancelled state"
},
{
"name": "Payment failure compensation — customer notification",
"max_score": 8,
"description": "On payment failure event, a customer notification is triggered (calls notifyCustomer or equivalent)"
},
{
"name": "schemaVersion in event payload",
"max_score": 12,
"description": "Event payloads include a 'schemaVersion' field (or equivalent version identifier) in the Detail object"
},
{
"name": "Outbox pattern reference",
"max_score": 18,
"description": "Code or comments reference the outbox pattern (transactional outbox) to guarantee event publication after a database write, either as an implementation or an explicit architectural note"
}
]
}
Order Checkout Workflow Across Multiple Services
Problem Description
An e-commerce startup has decomposed their platform into separate services: an Order Service, a Payment Service, an Inventory Service, and a Fulfillment Service. When a customer completes checkout, a sequence of operations must happen: the order is recorded, payment is captured, inventory is reserved, and fulfillment is scheduled. If any step fails — particularly if payment capture fails — the system must roll back previously completed steps to leave the system in a consistent state (e.g. reserved inventory must be released, the order status updated, and the customer notified).
The team has been running into silent failures where a payment processor timeout leaves the order in a "pending" limbo — inventory held but the customer never charged, with no notification sent. They need a robust approach where each service publishes structured events, other services react, and failures trigger compensating actions automatically.
The engineering lead also wants each event payload to carry version metadata so that when event schemas evolve in the future, older consumers don't silently break.
Output Specification
Implement the event-driven order workflow in TypeScript. Produce the following files:
services/order-service/handlers.ts— event handlers for the order service (creating order, handling payment failure)services/payment-service/handlers.ts— event handler that captures payment on order creationservices/inventory-service/handlers.ts— event handler that reserves stock and handles order cancellationservices/fulfillment-service/handlers.ts— event handler that schedules fulfillment on payment successservices/shared/events.ts— event type definitions and a publish helper
The code should demonstrate the full happy path and the payment-failure compensation path. Code does not need to connect to live infrastructure but should be complete and correct TypeScript.
{
"name": "finsi/composable-commerce",
"version": "0.1.0",
"summary": "MACH architecture — microservices, API-first, cloud-native, headless patterns",
"skills": {
"composable-commerce": {
"path": "SKILL.md"
}
}
}