mcpField
C
composable-commerce

composable-commerce

github.com/finsilabs開発6076 回インストール5 日前に更新
リポジトリ

composable-commerce について

カスタムまたはヘッドレスのストアフロントを構築するチーム向けに、MACH に基づくコンポーザブルコマースを解説します。サービス境界、API の集約、イベント連携、本番運用を扱います。

主な機能

  1. 01コマース機能ごとのサービス分割
  2. 02BFF による API 集約
  3. 03イベントによるサービス連携
  4. 04分散トランザクション向け Saga
  5. 05API のバージョンと互換性管理
  6. 06サーキットブレーカーによる耐障害性向上

活用シーン

  1. 01モノリシックなコマース基盤の分割
  2. 02異なるベンダーのコマース機能の組み合わせ
  3. 03単一バックエンドで複数ストアフロントを支援
  4. 04注文・在庫・フルフィルメントの連携

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill composable-commerce

インストール後、Claude Code で/composable-commerceを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/api-versioning-and-distributed-tracing/
criteria.json
task.md
evals/bff-aggregation-with-circuit-breakers/
criteria.json
task.md
evals/event-driven-saga-with-compensating-tran/
criteria.json
task.md

詳細

コンポーザブルコマース

概要

コンポーザブルコマースは、MACH の原則(マイクロサービス、API ファースト、クラウドネイティブ、ヘッドレス)に基づくアーキテクチャアプローチです。カート、カタログ、検索、CMS、チェックアウト、ロイヤルティなどの各コマース機能は、モノリシックなプラットフォームではなく、それぞれの分野で最適なサービスによって提供されます。サービスは API とイベントを介して通信するため、チームはシステムのほかの部分に手を加えることなく、個々の機能を置き換えたりアップグレードしたりできます。このスキルでは、MACH アーキテクチャのパターン、サービス統合、イベント駆動型の連携、コンポーザブルスタックを本番環境で運用する際の考慮事項について扱います。

このスキルを使用する場面

  • モノリシックなプラットフォーム(Magento、Salesforce CC)では、チームやトラフィックのパターンに対応しきれなくなった場合
  • ドメイン(カタログ、チェックアウト、ロイヤルティ)ごとに、リリースの頻度や担当チームが異なる場合
  • 各分野で最適なベンダーを組み合わせる必要がある場合(例:検索に Algolia、CMS に Contentful、コマースに commercetools を使用する)
  • 地域ごとに異なるフルフィルメント、価格設定、税金のプロバイダーが必要な新しい市場に参入する場合
  • 単一のバックエンドから複数のストアフロント(ウェブ、モバイル、実店舗、B2B ポータル)をサポートするプラットフォームを構築する場合

前提条件とプラットフォームに関する注意事項

このスキルはカスタム/ヘッドレスストアフロント(Node.js、Python、または同様のバックエンド)向けに作成されています。コード例では TypeScript/Node.js を使用していますが、どのようなスタックにも適応できます。

Shopify:Shopify Hydrogen は Shopify のヘッドレスフレームワークです。Shopify をコマースバックエンドとしてカスタムフロントエンドと組み合わせる場合や、Shopify を各分野で最適なほかのサービスと組み合わせる場合に、MACH/コンポーザブルのパターンを適用できます。 WooCommerce:WooCommerce は REST API と WPGraphQL を介して、ヘッドレスバックエンドとして利用できます。これらのパターンは、フロントエンドを WordPress から切り離す場合に適用できます。 Magento:Magento の GraphQL API と PWA Studio は、ヘッドレスアーキテクチャをサポートしています。これらのコンポーザブルパターンは、MACH スタックのバックエンドサービスとして Magento を使用する場合に適用できます。

必要なもの:

  • Node.js 18 以降(またはバックエンドの言語に合わせて調整してください)
  • PostgreSQL(または任意のリレーショナルデータベース)
  • 検索サービス(Algolia、Elasticsearch、または Typesense)
  • Stripe アカウントと API キー
  • メール送信サービス(SendGrid、AWS SES、または Postmark)

コアとなる指針

  1. コマース機能を中心にサービスの境界を設計する

    一般的なコンポーザブルコマースのスタックでは、機能を個別のサービスに分割します。

    機能サービスの例
    商品カタログcommercetools, Akeneo PIM, Salsify
    検索と発見Algolia, Elasticsearch, Constructor.io
    CMS/コンテンツContentful, Sanity, Storyblok
    カートとチェックアウトcommercetools, Elastic Path, Medusa
    決済Stripe, Adyen, Braintree
    税金Avalara, TaxJar
    配送とフルフィルメントEasyPost, ShipBob, カスタム OMS
    顧客 IDAuth0, Okta, Cognito
    ロイヤルティとプロモーションTalon.One, Voucherify
    メール/通知SendGrid, Customer.io

    境界づけられたコンテキストを定義します。各サービスは自身のデータを管理し、API 経由でのみ公開します。サービス間でデータベースを共有してはいけません。

  2. API 合成レイヤー(BFF)を実装する

    バックエンド・フォー・フロントエンド(BFF)は、複数の上流 API をフロントエンドのニーズに合わせて集約し、単一のリクエストにまとめます。

    // 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,
      };
    }
    

    スキーマスティッチングまたはフェデレーションを使用して、BFF を GraphQL API として公開します。

    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. サービス間の連携にイベント駆動型アーキテクチャを使用する

    複数の機能にまたがるワークフロー(注文確定 → 在庫引当 → フルフィルメント開始 → メール送信)では、サービス間で非同期に通信します。

    // 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. 分散トランザクションに Saga パターンを実装する

    複数のサービスにまたがる多段階のワークフローをアトミックに実行する必要がある場合は、補償トランザクションを使用するオーケストレーション型の Saga を使います。

    // 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. API のバージョン管理と後方互換性を維持する

    コンポーザブルスタックでは、サービスがそれぞれ独立して進化します。追加型のバージョン管理戦略を使用します。

    // 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. レジリエンスを高めるサーキットブレーカーを実装する

    あるサービスの性能が低下したとき、スタック全体に障害が連鎖するのを防ぎます。

    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);
    

例

カタログ向け commercetools SDK 統合
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();
Contentful CMS によるデータ拡充を伴う Algolia 検索
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),
  }));
}

ベストプラクティス

  • 明確なデータ所有モデルを定義する — データの各要素について、信頼できる唯一の情報源となるシステムを必ず1つ定めます。他のサービスは API 経由で読み取り、直接書き込んではいけません。
  • 結果整合性を前提に設計する — イベント発生後、サービス間のデータには一時的な不整合が生じます。これを許容できる UI とワークフロー(楽観的な在庫表示、補償トランザクションなど)を構築します。
  • 横断的な関心事には API ゲートウェイを使用する — 認証、レート制限、リクエストのロギング、SSL 終端は、各サービスではなくゲートウェイで処理します。
  • 分散トレーシングを実装する — OpenTelemetry を使用し、すべてのサービス呼び出しでトレース ID を伝播させます。これは、複数サービスにまたがるリクエスト障害のデバッグに不可欠です。
  • イベントにバージョンを付ける — イベントスキーマは時間とともに変化します。各イベントのペイロードに schemaVersion フィールドを含め、後方互換性のあるコンシューマーを維持します。
  • サービス間トラフィックにはサービスメッシュを使用する — Istio や AWS App Mesh などのツールは、インフラストラクチャレイヤーで相互 TLS、ロードバランシング、リトライを処理します。
  • まずはモジュラーモノリスから始める — 明確なチームの所有境界と運用成熟度が整ってから、マイクロサービスに分割します。時期尚早な分割は、意図しない複雑さを生みます。

よくある落とし穴

問題解決策
分散トランザクションの失敗によりデータに不整合が生じる補償トランザクションを伴うサガを実装します。DB への書き込み後にイベントが確実に発行されるよう、アウトボックスパターンを使用します
クリティカルパスでサービスのレイテンシーが累積する非同期イベントを使用して、クリティカルパスから重要度の低いサービスを外します。すべての外部呼び出しに厳格なタイムアウトとサーキットブレーカーを設定します
API コントラクトの変更により、下流のコンシューマーに影響が出るコンシューマー駆動型コントラクトテスト(Pact)を使用し、破壊的変更をデプロイ前に検出します
共有データベースによって隠れた結合が生じる「1 サービスにつき 1 スキーマ」のルールを徹底します。サービス間のデータ伝播には、イベントソーシングまたは変更データキャプチャを使用します
10 個のサービスにまたがる障害のデバッグ初日から OpenTelemetry による分散トレーシングを実装します。ユーザーリクエストごとに単一のトレース ID を使用してログを関連付けます

関連スキル

  • @commerce-api-gateway
  • @saleor-development
  • @shopify-hydrogen
  • @webhook-architecture
  • @flash-sale-scaling
  • @edge-commerce

コメント