Adobe Commerce Optimizer: Das Unified Data Model unter der Haube

Vor kurzem habe ich bei einem Adobe Event das Produkt Adobe Commerce Optimizer (ACO) vorgestellt. Bei der Vorstellung wollte ich nicht mit den Standard-Demodaten arbeiten und wollte meine eigenen Demo-Daten nutzen. Dazu musste ich folgende Dinge klären: Welche API nimmt Daten entgegen? Wie ist das Datenmodell strukturiert? Und wie kommt das Ergebnis in mein Storefront (oder eigenes Frontend)?

Was ist das Unified Data Model?

Hinter ACO steckt eine SaaS-Schicht namens Merchandising Services, die auf einem zentralisierten, quellenunabhängigen Datenspeicher basiert. Adobe spricht in diesem Zusammenhang von der CCDM-Architektur – Composable Catalog Data Model.

Der Kerngedanke: Produktdaten kommen aus beliebigen Quellen (ERP, PIM, Magento, Shopify, was auch immer), landen in einem einzigen Basiskatalog und werden dort nach einem einheitlichen Modell gespeichert. Die Storefront greift über GraphQL darauf zu – gefiltert durch Catalog Views und Policies (Regelwerk).

Das Standardfrontend ist die neue Adobe Commerce Storefront auf Basis von Edge Delivery Services (EDS). Diese Storefront ist darauf ausgelegt, Produktdaten direkt über die GraphQL-Schicht von ACO zu konsumieren – ohne klassischen Magento-Applikationsstack dazwischen. Wer ein eigenes Headless-Frontend betreiben will, kann ebenfalls direkt auf den GraphQL-Endpoint aufsetzen; die EDS-Storefront bleibt aber der von Adobe vorgesehene und gepflegte Standardweg.

Datenerfassung: REST rein

Daten kommen über eine RESTful Data Ingestion API in den ACO. Die Base-URL folgt diesem Schema:

https://{region}-{environment}.api.commerce.adobe.com/{tenantId}

Alle Requests benötigen einen Authorization-Header mit Bearer-Token (IMS-Credential-basiert) sowie Content-Type: application/json.

Schritt 1: Attribut-Metadaten definieren

Bevor ein einziges Produkt importiert werden kann, müssen die Attribut-Metadaten angelegt werden – und das pro Locale. ACO muss vorab wissen, wie es mit jedem Attribut umgehen soll: Ist es filterbar? Sortierbar? Suchbar? Mit welchem Gewicht?

curl -X POST \
  'https://na1-sandbox.api.commerce.adobe.com/{tenantId}/v1/catalog/products/metadata' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {accessToken}' \
  -d '[
    {
      "code": "name",
      "source": { "locale": "en-US" },
      "label": "Product Name",
      "dataType": "TEXT",
      "visibleIn": ["PRODUCT_DETAIL", "PRODUCT_LISTING", "SEARCH_RESULTS"],
      "filterable": false,
      "sortable": true,
      "searchable": true,
      "searchWeight": 1,
      "searchTypes": ["AUTOCOMPLETE"]
    },
    {
      "code": "brand",
      "source": { "locale": "en-US" },
      "label": "Brand",
      "dataType": "TEXT",
      "visibleIn": ["PRODUCT_LISTING", "SEARCH_RESULTS"],
      "filterable": true,
      "sortable": false,
      "searchable": true,
      "searchWeight": 1,
      "searchTypes": ["AUTOCOMPLETE", "CONTAINS", "STARTS_WITH"]
    }
  ]'

Der dataType kann TEXT, DECIMAL, BOOLEAN oder INT sein. visibleIn steuert, wo das Attribut auf der Storefront erscheint: Produktdetailseite, Listing, Suchergebnisse, Vergleich.

Schritt 2: Produkte anlegen

Danach kommen die eigentlichen Produkte per POST. Das Produkt-Objekt trägt neben den Pflichtfeldern (sku, source, name, slug, status) alle benutzerdefinierten Attribute als flache Key/Value-Liste im attributes-Array. Über routes wird das Produkt Kategorien zugeordnet.

curl -X POST \
  'https://na1-sandbox.api.commerce.adobe.com/{tenantId}/v1/catalog/products' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {accessToken}' \
  -d '[
    {
      "sku": "aurora-battery-pro-100",
      "source": { "locale": "en-US" },
      "name": "Aurora Battery Pro 100",
      "slug": "aurora-battery-pro-100.html",
      "status": "ENABLED",
      "description": "High-performance 100Ah lithium battery for electric vehicles.",
      "shortDescription": "100Ah LiFePO4 traction battery",
      "visibleIn": ["CATALOG", "SEARCH"],
      "metaTags": {
        "title": "Aurora Battery Pro 100",
        "description": "100Ah LiFePO4 battery for EVs",
        "keywords": ["battery", "aurora", "electric vehicle"]
      },
      "attributes": [
        { "code": "brand", "values": ["Aurora"] },
        { "code": "location", "values": ["US"] },
        { "code": "capacity_ah", "values": ["100"] }
      ],
      "images": [
        {
          "url": "https://example.com/images/aurora-battery-pro-100.jpg",
          "label": "Aurora Battery Pro 100",
          "roles": ["BASE", "SMALL"],
          "customRoles": []
        }
      ],
      "routes": [
        { "path": "batteries" },
        { "path": "batteries/lithium", "position": 1 }
      ]
    }
  ]'

Auffällig: Alle Attributwerte – egal ob TEXT, DECIMAL oder INT – werden als Strings in values übergeben. ACO konvertiert sie intern anhand der vorher definierten Metadaten. An manchen Stellen würde ich mir etwas mehr Tiefe im Datenmodell wünschen, um auch kompliziertere Datenstrukturen (z. B. strukturierte Variantenkonfigurationen) sauber abbilden zu können. Das Array-Modell ist funktional, aber manchmal etwas flach.

Schritt 3: Price Books und Preise

Price Books ermöglichen unterschiedliche Preisstrukturen für verschiedene Kundensegmente, Regionen oder Kanäle – ohne den Basiskatalog zu verändern. Price Books können hierarchisch aufgebaut werden: Ein Basis-Price-Book legt die Währung fest, Kind-Price-Books erben davon und können selektiv überschreiben.

Zunächst werden die Price Books selbst angelegt:

curl -X POST \
  'https://na1-sandbox.api.commerce.adobe.com/{tenantId}/v1/catalog/price-books' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {accessToken}' \
  -d '[
    {
      "priceBookId": "us",
      "name": "US Base Price Book",
      "currency": "USD"
    },
    {
      "priceBookId": "us-retail",
      "parentId": "us",
      "name": "US Retail"
    },
    {
      "priceBookId": "us-wholesale",
      "parentId": "us",
      "name": "US Wholesale"
    },
    {
      "priceBookId": "eu",
      "name": "EU Base Price Book",
      "currency": "EUR"
    }
  ]'

Ein Price Book ohne parentId ist ein Root-Price-Book und muss eine currency definieren. Kind-Price-Books erben die Währung vom Parent und brauchen kein eigenes currency-Feld – nur eine parentId.

Danach werden die Preise pro SKU und Price Book über einen separaten Endpoint gepflegt:

curl -X POST \
  'https://na1-sandbox.api.commerce.adobe.com/{tenantId}/v1/catalog/products/prices' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {accessToken}' \
  -d '[
    {
      "sku": "aurora-battery-pro-100",
      "regular": 899.99,
      "priceBookId": "us"
    },
    {
      "sku": "aurora-battery-pro-100",
      "regular": 749.99,
      "priceBookId": "us-wholesale"
    },
    {
      "sku": "aurora-battery-pro-100",
      "regular": 849.00,
      "priceBookId": "eu"
    }
  ]'

Dasselbe Produkt bekommt also je nach Price Book einen anderen Preis. Welchen Preis die GraphQL-API zurückliefert, steuert der AC-Price-Book-ID-Header im Storefront-Request – ohne dass dafür die Produktdaten angepasst werden müssten.

Das Datenmodell: Catalog Views und Policies

Der eigentlich interessante Teil der CCDM-Architektur ist nicht der Import, sondern die Filterschicht:

  • Catalog Views definieren eine Geschäftseinheit: ein Händler, eine Marke, ein Markt. Jede View ist mit einer Katalogquelle (Locale) und einer Menge von Policies verknüpft.
  • Policies sind Datenzugriffsfilter auf Basis von Produktattributen. Eine Policy Brand mit Wert Aurora liefert beispielsweise ausschließlich Produkte, die das Attribut brand = Aurora tragen.

Das bedeutet: Derselbe Basiskatalog kann über verschiedene Catalog Views mit verschiedenen Policies völlig unterschiedliche Produktmengen ausliefern – ohne Datenduplizierung. Für Szenarien mit mehreren Marken, Märkten oder Vertriebskanälen ist das ein erheblicher architektonischer Vorteil.

Ein konkretes Beispiel aus der offiziellen Dokumentation: Ein fiktives Unternehmen (Zenith Automotive) pflegt einen einzigen Katalog für zwei Marken und zwei Märkte. Statt vier separate Instanzen zu betreiben, wird ein View mit zwei Policies konfiguriert:

  • Policy Location filtert nach Zielmarkt (USA, UK)
  • Policy Brand filtert nach Marke (Aurora, Bolt)

Ein Produkt trägt die entsprechenden Attribute. Welche Produkte ein API-Aufruf zurückliefert, hängt ausschließlich von den übergebenen Policy-Headern ab – nicht von der Datenstruktur selbst.

Datenzugriff: GraphQL raus

Die Storefront – ob EDS-basiert oder eine eigene Headless-Implementierung – greift auf die Daten per GraphQL Merchandising API zu. Authentifizierung ist auf dieser Ebene nicht erforderlich; die Steuerung läuft über HTTP-Header:

Header Bedeutung
AC-View-ID Pflichtfeld – welche Catalog View soll verwendet werden?
AC-Policy-{Name} Optional – Filterung nach Policy-Wert, z. B. AC-Policy-Brand: Aurora
AC-Price-Book-ID Optional – welches Price Book soll für die Preisberechnung genutzt werden?

Eine typische Produktsuche sieht so aus:

curl -X POST \
  'https://na1-sandbox.api.commerce.adobe.com/{tenantId}/graphql' \
  -H 'Content-Type: application/json' \
  -H 'AC-View-ID: {catalogViewId}' \
  -H 'AC-Policy-Brand: Aurora' \
  -H 'AC-Price-Book-ID: us-wholesale' \
  -d '{
    "query": "query ProductSearch($search: String!) {
      productSearch(phrase: $search, page_size: 10) {
        items {
          productView {
            sku
            name
            description
            images { url }
            ... on SimpleProductView {
              price {
                regular {
                  amount { value currency }
                }
              }
            }
          }
        }
      }
    }",
    "variables": { "search": "battery" }
  }'

Die Response enthält ausschließlich Produkte, die zur konfigurierten View und Policy passen. Das Frontend muss nur die richtigen Header mitschicken – die gesamte Kataloglogik liegt in der ACO-Konfiguration.

Praktische Use Cases

Wo ist dieser Ansatz in der Praxis relevant?

Multi-Brand-Händler ohne Datensilos: Wer heute für jede Marke eine separate Magento-Instanz betreibt, kennt den Aufwand: Doppelte Pflege, doppelte Synchronisation, doppelter Upgrade-Aufwand. Mit CCDM ist das ein einziger Basiskatalog mit konfigurierten Views – und die organisatorische Trennung bleibt über Policies erhalten.

Internationalisierung ohne Datenduplizierung: Verschiedene Märkte, verschiedene Preise, verschiedene Sortimente – über Price Books und Policies steuerbar, ohne den Katalog zu kopieren.

Headless-Storefronts mit beliebigem Backend: ACO ist explizit darauf ausgelegt, dass das datenliefernde System kein Adobe Commerce sein muss. ERP, PIM, Shopify, ein eigenes System – alles kann über die REST Ingestion API einspeisen. Die EDS-Storefront konsumiert dann via GraphQL. Wer die EDS-Storefront nicht nutzen will, kann mit einem eigenen Frontend direkt an den GraphQL-Endpoint andocken. Ich habe das exemplarisch umgesetzt unter aco.demo.muench.dev.

Warenkorb und Checkout bleiben flexibel: ACO stellt bewusst keinen Warenkorb bereit. Der wird über API Mesh oder App Builder an ein bestehendes System angebunden. Das ist pragmatisch – und verhindert, dass für einen Storefront-Umbau die gesamte Transaktionslogik angefasst werden muss.

Ehrliche Einschätzung

Das Modell ist relativ einfach aufgebaut (mir manchmal zu einfach). Die klare Trennung zwischen Ingestion (REST), Konfiguration (Catalog Views/Policies) und Delivery (GraphQL) ist sauber durchdacht. Gerade für komplexe Multi-Brand- oder Multi-Market-Setups ist das eine Architektur, die in klassischen Magento-Projekten mit erheblich mehr Aufwand gebaut werden muss.

Allerdings bietet ACO keine Transaktionslogik direkt von Haus aus an. Alles was komplizierter ist, muss momentan selbst angebaut werden.

Dazu kommt: ACO ist SaaS-only. Für Händler, die ihre Datenhaltung im eigenen Rechenzentrum behalten müssen oder wollen – ein Thema, das ich im Tech-Trends-Post für 2026 unter dem Stichwort Sovereign Commerce ausgeführt habe – ist ACO in seiner aktuellen Form keine Option.

Für die richtige Zielgruppe dagegen – Enterprise-Händler mit komplexen Katalogstrukturen, die ihren bestehenden Backend-Stack behalten und die Storefront-Experience modernisieren wollen – ist ACO ein überzeugender Ansatz.

Für alle die vielleicht schon eine sehr heterogene Landschaft haben und sowieso vieles auf Services verteilt haben, ist ACO echt spannend. Die Stärken von ACO sind auch immer in Verbindung mit einem hochskalierenden Frontend auf Basis von Adobe Edge Delivery Services zu sehen. Wer nur einach Daten halten und ausspielen will, für den ist ACO vielleicht nicht das richtige. Das heisst aber nicht, dass es nicht auch spannend sein kann ein eigenes Frontend direkt auf die GraphQL Schnittstelle zu setzen.

Fazit

Das Unified Data Model hinter Adobe Commerce Optimizer ist keine Abstraktionsschicht ohne Substanz. Es ist ein einfaches, quellenunabhängiges Katalogmodell: Daten kommen per REST-API rein, werden über Catalog Views und Policies konfiguriert und per GraphQL ausgeliefert – an die EDS-Storefront oder ein eigenes Frontend.

Wer das selbst erkunden will: Die offizielle Dokumentation ist inzwischen gut aufgestellt. Einstiegspunkte sind die Developer-Doku zur Data Ingestion API und der End-to-End Use Case in der Adobe-Dokumentation. Ihr braucht dann zum Ausprobieren eine Sandbox von Adobe oder lasst es euch von mir zeigen. :-)