Adobe Commerce Optimizer: The Unified Data Model Under the Hood

Adobe Commerce Optimizer: The Unified Data Model Under the Hood

Recently, I presented Adobe Commerce Optimizer (ACO) at an Adobe event. For the presentation, I did not want to work with the standard demo data and wanted to use my own instead. To do that, I had to clarify the following things: which API accepts data? How is the data model structured? And how does the result reach my storefront (or custom frontend)?

What Is the Unified Data Model?

Behind ACO is a SaaS layer called Merchandising Services, based on a centralized, source-independent data store. Adobe refers to this as the CCDM architecture — Composable Catalog Data Model.

The core idea: product data comes from any source (ERP, PIM, Magento, Shopify, whatever), ends up in a single base catalog, and is stored there according to a unified model. The storefront accesses it through GraphQL — filtered by Catalog Views and Policies (a set of rules).

The standard frontend is the new Adobe Commerce Storefront based on Edge Delivery Services (EDS). This storefront is designed to consume product data directly through ACO's GraphQL layer — without a traditional Magento application stack in between. Anyone wanting to run their own headless frontend can also build directly on the GraphQL endpoint; however, the EDS storefront remains the standard route intended and maintained by Adobe.

Data Ingestion: REST In

Data enters ACO through a RESTful Data Ingestion API. The base URL follows this pattern:

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

All requests require an Authorization header with a bearer token (based on IMS credentials), as well as Content-Type: application/json.

Step 1: Define Attribute Metadata

Before even a single product can be imported, the attribute metadata must be created — for each locale. ACO needs to know in advance how to handle each attribute: is it filterable? Sortable? Searchable? With what weight?

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"]
    }
  ]'

The dataType can be TEXT, DECIMAL, BOOLEAN, or INT. visibleIn controls where the attribute appears on the storefront: product detail page, listing, search results, comparison.

Step 2: Create Products

Next, the actual products are submitted via POST. Alongside the required fields (sku, source, name, slug, status), the product object carries all custom attributes as a flat key/value list in the attributes array. Products are assigned to categories through routes.

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 }
      ]
    }
  ]'

One notable detail: all attribute values — whether TEXT, DECIMAL, or INT — are passed as strings in values. ACO converts them internally based on the metadata defined earlier. In some places, I would like a little more depth in the data model so that more complex data structures (such as structured variant configurations) could also be represented cleanly. The array model is functional, but sometimes a little flat.

Step 3: Price Books and Prices

Price Books enable different pricing structures for different customer segments, regions, or channels — without changing the base catalog. Price Books can be organized hierarchically: a base Price Book defines the currency, while child Price Books inherit from it and can selectively override values.

First, the Price Books themselves are created:

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"
    }
  ]'

A Price Book without a parentId is a root Price Book and must define a currency. Child Price Books inherit the currency from their parent and do not need their own currency field — only a parentId.

Next, prices per SKU and Price Book are maintained through a separate endpoint:

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"
    }
  ]'

The same product therefore gets a different price depending on the Price Book. The AC-Price-Book-ID header in the storefront request controls which price the GraphQL API returns — without needing to adjust the product data.

The Data Model: Catalog Views and Policies

The genuinely interesting part of the CCDM architecture is not the import, but the filtering layer:

  • Catalog Views define a business entity: a retailer, a brand, a market. Each view is linked to a catalog source (locale) and a set of policies.
  • Policies are data access filters based on product attributes. A policy Brand with the value Aurora, for example, returns only products with the attribute brand = Aurora.

This means the same base catalog can deliver completely different sets of products through different Catalog Views with different policies — without duplicating data. For scenarios involving multiple brands, markets, or sales channels, that is a significant architectural advantage.

A concrete example from the official documentation: a fictional company (Zenith Automotive) maintains a single catalog for two brands and two markets. Instead of running four separate instances, a view is configured with two policies:

  • Policy Location filters by target market (USA, UK)
  • Policy Brand filters by brand (Aurora, Bolt)

A product carries the corresponding attributes. Which products an API call returns depends entirely on the policy headers passed — not on the data structure itself.

Data Access: GraphQL Out

The storefront — whether EDS-based or a custom headless implementation — accesses the data through the GraphQL Merchandising API. Authentication is not required at this level; control is handled through HTTP headers:

Header Meaning
AC-View-ID Required — which Catalog View should be used?
AC-Policy-{Name} Optional — filter by policy value, e.g. AC-Policy-Brand: Aurora
AC-Price-Book-ID Optional — which Price Book should be used to calculate prices?

A typical product search looks like this:

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" }
  }'

The response contains only products that match the configured view and policy. The frontend only needs to send the right headers — all the catalog logic resides in the ACO configuration.

Practical Use Cases

Where is this approach relevant in practice?

Multi-brand retailers without data silos: Anyone currently running a separate Magento instance for each brand knows the effort involved: duplicated maintenance, duplicated synchronization, duplicated upgrade work. With CCDM, it is a single base catalog with configured views — and organizational separation is preserved through policies.

Internationalization without data duplication: Different markets, different prices, different product ranges — controlled through Price Books and policies without copying the catalog.

Headless storefronts with any backend: ACO is explicitly designed so that the system supplying the data does not have to be Adobe Commerce. ERP, PIM, Shopify, a custom system — anything can feed data through the REST Ingestion API. The EDS storefront then consumes it via GraphQL. Anyone not wanting to use the EDS storefront can connect their own frontend directly to the GraphQL endpoint. I have implemented an example of this at aco.demo.muench.dev.

Cart and checkout remain flexible: ACO deliberately does not provide a shopping cart. It is connected to an existing system through API Mesh or App Builder. That is pragmatic — and prevents a storefront redesign from requiring changes to all the transaction logic.

An Honest Assessment

The model is relatively simple (sometimes too simple for me). The clear separation between ingestion (REST), configuration (Catalog Views/Policies), and delivery (GraphQL) is thoughtfully designed. Particularly for complex multi-brand or multi-market setups, this is an architecture that takes significantly more effort to build in traditional Magento projects.

However, ACO does not offer transaction logic out of the box. Anything more complicated currently has to be added yourself.

On top of that: ACO is SaaS-only. For retailers who need or want to keep their data in their own data center — a topic I explored in the tech trends post for 2026 under the heading Sovereign Commerce — ACO in its current form is not an option.

For the right target audience, however — enterprise retailers with complex catalog structures who want to retain their existing backend stack and modernize the storefront experience — ACO is a convincing approach.

For anyone who may already have a very heterogeneous landscape and has distributed a lot across services anyway, ACO is genuinely exciting. ACO's strengths should always be considered in combination with a highly scalable frontend based on Adobe Edge Delivery Services. If you simply want to store and serve data, ACO may not be the right choice. But that does not mean it cannot also be interesting to build your own frontend directly on the GraphQL interface.

Conclusion

The Unified Data Model behind Adobe Commerce Optimizer is not an abstraction layer without substance. It is a simple, source-independent catalog model: data comes in through a REST API, is configured through Catalog Views and policies, and is delivered through GraphQL — to the EDS storefront or a custom frontend.

If you want to explore it yourself: the official documentation is now in good shape. Starting points are the developer documentation for the Data Ingestion API and the end-to-end use case in Adobe's documentation. To try it out, you will need a sandbox from Adobe — or let me show you. :-)