DEV Community

Aliaksandr Tsviatkou
Aliaksandr Tsviatkou

Posted on

SAP Commerce Cloud Architecture Demystified: A Practical Guide for Developers

If you're a Java developer stepping into the SAP Commerce world — or a mid-level Commerce developer who wants to truly understand what's happening under the hood — this article is for you. We'll go beyond surface-level overviews and dig into the internals: the class hierarchies, the extension loading mechanism, the type system's relationship to the database, and the design patterns that make the platform tick.

High-Level Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        CLIENT LAYER                             │
│   Composable Storefront (Spartacus)  │  Mobile  │  Custom SPA  │
└──────────────────────┬──────────────────────────────────────────┘
                       │ HTTPS (REST/JSON)
┌──────────────────────▼──────────────────────────────────────────┐
│                    OCC REST API LAYER                            │
│   @Controller classes in ycommercewebservices / custom OCC ext   │
│   WsDTO objects  │  Orika Mappers  │  OAuth2 (spring-security)  │
└──────────────────────┬──────────────────────────────────────────┘
                       │ Java method calls
┌──────────────────────▼──────────────────────────────────────────┐
│                    APPLICATION LAYER                             │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │  Facades     │  │  Services    │  │  Strategies / Hooks    │ │
│  │ (DTOs out)   │→ │ (Business    │→ │ (FindPriceStrategy,    │ │
│  │              │  │  Logic)      │  │  AddToCartStrategy...) │ │
│  └─────────────┘  └──────┬───────┘  └────────────────────────┘ │
│                          │                                      │
│  ┌───────────────────────▼───────────────────────────────────┐  │
│  │  DAOs (FlexibleSearchQuery → GenericSearchService)        │  │
│  └───────────────────────┬───────────────────────────────────┘  │
│                          │                                      │
│  ┌───────────────────────▼───────────────────────────────────┐  │
│  │  Model Objects (generated from items.xml)                 │  │
│  └───────────────────────────────────────────────────────────┘  │
└──────────────────────┬──────────────────────────────────────────┘
                       │ JDBC
┌──────────────────────▼──────────────────────────────────────────┐
│                    PERSISTENCE LAYER                             │
│   RDBMS (SAP HANA / MySQL / HSQLDB / Oracle / PostgreSQL)       │
│   + Solr (product search)  + Media Storage (local / S3 / Azure) │
└─────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Client Layer

The client layer is anything that consumes Commerce APIs. In modern projects, this is typically Composable Storefront (Spartacus) — an Angular SPA that communicates exclusively via OCC REST APIs. Legacy projects may still use the JSP-based Accelerator storefront, which runs inside the Commerce JVM itself.

OCC REST API Layer

The OCC (Omni Commerce Connect) layer exposes RESTful endpoints under /occ/v2/. The entry point classes live in the ycommercewebservices extension (or your custom OCC extension). Key internals:

  • Controllers extend Spring's @Controller and use @RequestMapping annotations
  • WsDTO classes (Data Transfer Objects) are the API contract — generated or hand-crafted POJOs annotated for serialization
  • Orika Mappers (Converter<Source, Target>) translate between internal Models/Data objects and WsDTOs
  • OAuth2 authentication is handled via Spring Security OAuth, configured in ycommercewebservices Spring context

Application Layer

This is where business logic lives. The critical class hierarchy:

  • Facades (e.g., DefaultProductFacade, DefaultCartFacade) — orchestrate multiple services and convert Models into Data objects (DTOs). Facades implement interfaces like ProductFacade, CartFacade.
  • Services (e.g., DefaultProductService, DefaultCartService) — contain business logic. They extend AbstractBusinessService or implement interfaces like ProductService, CartService. They operate on Model objects.
  • DAOs (e.g., DefaultProductDao) — execute FlexibleSearchQuery objects via FlexibleSearchService and return Model objects.
  • Strategies — pluggable business rules. For example, FindPriceStrategy determines product pricing, CommerceAddToCartStrategy handles add-to-cart logic.

Platform Layer

Under all the commerce-specific code lies the platform itself. Key platform classes:

  • Registry — the bootstrap class. Registry.activateStandaloneMode() or Registry.activateMasterTenant() starts the platform.
  • Tenant — represents an isolated runtime context. MasterTenant is the primary tenant.
  • TypeManager — manages the runtime type system, loaded from items.xml definitions across all extensions.
  • ModelService — the persistence gateway. modelService.save(model), modelService.remove(model), modelService.create(MyModel.class).

Persistence Layer

SAP Commerce supports multiple databases through JDBC. In CCv2 (Cloud), SAP HANA is the standard database. The platform generates DDL from the type system definitions — you never write CREATE TABLE statements manually.

Solr (via the solrfacetsearch extension) provides full-text product search with faceting. ZooKeeper manages the Solr cluster in CCv2.

The Type System

The type system is what makes SAP Commerce fundamentally different from a standard JPA/Hibernate application. Instead of writing @Entity classes and Flyway migrations, you define your data model in items.xml files, and the platform generates both Java Model classes and database DDL.

items.xml Deep Dive

Here's a realistic items.xml example:

<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="items.xsd">

    <enumtypes>
        <enumtype code="LoyaltyTier" autocreate="true" generate="true" dynamic="true">
            <value code="BRONZE"/>
            <value code="SILVER"/>
            <value code="GOLD"/>
            <value code="PLATINUM"/>
        </enumtype>
    </enumtypes>

    <itemtypes>
        <itemtype code="Customer" autocreate="false" generate="false">
            <!-- Extending the existing Customer type -->
            <attributes>
                <attribute qualifier="loyaltyTier" type="LoyaltyTier">
                    <modifiers optional="true"/>
                    <persistence type="property"/>
                </attribute>
                <attribute qualifier="loyaltyPoints" type="java.lang.Integer">
                    <defaultvalue>Integer.valueOf(0)</defaultvalue>
                    <modifiers optional="true"/>
                    <persistence type="property"/>
                </attribute>
                <attribute qualifier="loyaltyScore" type="java.lang.Double">
                    <modifiers read="true" write="false" optional="true"/>
                    <persistence type="dynamic" attributeHandler="customerLoyaltyScoreHandler"/>
                </attribute>
            </attributes>
        </itemtype>

        <itemtype code="LoyaltyTransaction" autocreate="true" generate="true"
                  extends="GenericItem"
                  jaloclass="com.mycompany.jalo.LoyaltyTransaction">
            <deployment table="loyalty_transactions" typecode="25000"/>
            <attributes>
                <attribute qualifier="code" type="java.lang.String">
                    <modifiers unique="true" optional="false"/>
                    <persistence type="property"/>
                </attribute>
                <attribute qualifier="points" type="java.lang.Integer">
                    <modifiers optional="false"/>
                    <persistence type="property"/>
                </attribute>
                <attribute qualifier="transactionDate" type="java.util.Date">
                    <modifiers optional="false"/>
                    <persistence type="property"/>
                </attribute>
            </attributes>
            <indexes>
                <index name="codeIdx" unique="true">
                    <key attribute="code"/>
                </index>
                <index name="dateIdx">
                    <key attribute="transactionDate"/>
                </index>
            </indexes>
        </itemtype>
    </itemtypes>

    <relations>
        <relation code="Customer2LoyaltyTransaction" localized="false">
            <sourceElement type="Customer" qualifier="customer" cardinality="one">
                <modifiers optional="false"/>
            </sourceElement>
            <targetElement type="LoyaltyTransaction" qualifier="loyaltyTransactions" cardinality="many"
                           collectiontype="list" ordered="true">
                <modifiers partof="true"/>
            </targetElement>
        </relation>
    </relations>
</items>
Enter fullscreen mode Exit fullscreen mode

Key concepts:

  • autocreate="false" generate="false" on Customer: We're extending an existing platform type, not creating a new one. The platform merges attributes from all extensions' items.xml files for the same type code.
  • deployment: Maps the type to a specific database table and assigns a unique typecode (integer). Typecodes must be unique across the system (custom types should use 10000+).
  • persistence type="dynamic": The attribute isn't stored in the database. Instead, a DynamicAttributeHandler Spring bean computes the value at runtime.
  • persistence type="property": Standard database column storage.
  • partof="true" on the relation target: Defines a composition relationship — deleting the Customer will cascade-delete their LoyaltyTransactions.

How Types Map to the Database

When you run ant initialize or ant updatesystem, the platform's SchemaGenerator reads the merged type system and generates DDL:

  • Each itemtype with a deployment gets its own table
  • Types without a deployment store their attributes in the parent type's table (single-table inheritance)
  • Localized attributes get a separate *lp table (e.g., productsproductslp for localized product names)
  • Many-to-many relations create link tables
  • The props table stores attributes that overflow a type's dedicated table (configurable via impex.legacy.mode)

The internal class TypeManagerImpl builds the runtime ComposedType graph. You can inspect it at runtime:

TypeManager typeManager = TypeManager.getInstance();
ComposedType customerType = typeManager.getComposedType("Customer");
Set<AttributeDescriptor> attributes = customerType.getAttributeDescriptors();
Enter fullscreen mode Exit fullscreen mode

Generated Model Classes

After modifying items.xml and running ant build, the platform generates:

  • GeneratedLoyaltyTransactionModel.java in gensrc/ — generated getters/setters, do not edit
  • You create LoyaltyTransactionModel.java in src/ extending the generated class — add custom logic here

The generated model class extends ItemModel and uses the ModelService internally for persistence:

// Generated
public class GeneratedLoyaltyTransactionModel extends ItemModel {
    public static final String CODE = "code";
    public static final String POINTS = "points";

    public String getCode() {
        return getPersistenceContext().getPropertyValue(CODE);
    }

    public void setCode(String value) {
        getPersistenceContext().setPropertyValue(CODE, value);
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The PersistenceContext handles dirty tracking and lazy loading transparently.

Type System vs. Traditional ORM

Aspect JPA/Hibernate SAP Commerce Type System
Model definition @Entity Java annotations items.xml XML
Schema migration Flyway/Liquibase ant updatesystem
Model generation N/A (you write entities) Platform generates models from XML
Relationships @OneToMany, @ManyToMany <relation> elements in XML
Localization Manual (separate tables or JSON) Built-in via localized: prefix
Dynamic attributes N/A DynamicAttributeHandler
Query language JPQL/HQL FlexibleSearch
Inheritance @Inheritance strategies Type hierarchy in XML (extends=)

Configuration Hierarchy

SAP Commerce has a sophisticated configuration system with a clear precedence order. Understanding this is critical for debugging "why is my property not taking effect?"

Priority (highest wins):
─────────────────────────────────────────────
1. System properties      (-Dproperty=value)
2. Environment variables  (via y_ prefix convention in CCv2)
3. local.properties        (project-level overrides)
4. Extension properties   (myextension/project.properties)
5. Platform defaults      (platform/project.properties)
─────────────────────────────────────────────
Enter fullscreen mode Exit fullscreen mode

In Practice

# platform/project.properties (platform defaults)
db.pool.maxActive=30

# myextension/project.properties (extension defaults)
my.feature.enabled=false

# config/local.properties (project overrides, NOT in source control for local dev)
db.pool.maxActive=50
my.feature.enabled=true
db.url=jdbc:mysql://localhost:3306/hybris

# In CCv2 manifest.json (environment-specific)
{
  "properties": [
    { "key": "db.pool.maxActive", "value": "100", "persona": "production" },
    { "key": "my.feature.enabled", "value": "true" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Access configuration in code via:

Config.getParameter("my.feature.enabled");  // returns String
Config.getBoolean("my.feature.enabled", false);  // with default
Config.getInt("db.pool.maxActive", 30);
Enter fullscreen mode Exit fullscreen mode

The Config class reads from ConfigIntf, which loads all property sources at startup and merges them according to the precedence rules. In CCv2, environment-specific properties from manifest.json are injected as system properties or written to local.properties during the build.

Key Design Patterns

Populator / Converter Pattern

This is the most pervasive pattern in SAP Commerce. It separates the concern of converting Models to DTOs into small, composable units.

// Converter interface (from platform)
public interface Converter<SOURCE, TARGET> extends Populator<SOURCE, TARGET> {
    TARGET convert(SOURCE source);
    TARGET convert(SOURCE source, TARGET prototype);
}

// Populator interface
public interface Populator<SOURCE, TARGET> {
    void populate(SOURCE source, TARGET target);
}
Enter fullscreen mode Exit fullscreen mode

A typical converter delegates to a list of populators:

// Spring configuration
<bean id="loyaltyTransactionConverter" parent="abstractPopulatingConverter">
    <property name="targetClass" value="com.mycompany.data.LoyaltyTransactionData"/>
    <property name="populators">
        <list>
            <ref bean="loyaltyTransactionBasicPopulator"/>
            <ref bean="loyaltyTransactionPointsPopulator"/>
        </list>
    </property>
</bean>

<bean id="loyaltyTransactionBasicPopulator" 
      class="com.mycompany.facades.populators.LoyaltyTransactionBasicPopulator"/>
Enter fullscreen mode Exit fullscreen mode
public class LoyaltyTransactionBasicPopulator 
    implements Populator<LoyaltyTransactionModel, LoyaltyTransactionData> {

    @Override
    public void populate(LoyaltyTransactionModel source, LoyaltyTransactionData target) {
        target.setCode(source.getCode());
        target.setTransactionDate(source.getTransactionDate());
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this pattern matters: You can add new populators via Spring without modifying existing code. An extension can add a loyaltyTransactionCustomPopulator to the converter's populator list using Spring <list merge="true">, extending the DTO conversion without touching the original code. This is critical for upgradeability.

Strategy Pattern

Many business decisions in SAP Commerce are implemented as strategies — pluggable beans that can be swapped via Spring configuration.

Key strategies in the platform:

Strategy Interface Purpose Default Implementation
CommercePlaceOrderStrategy Place order logic DefaultCommercePlaceOrderStrategy
CommerceAddToCartStrategy Add to cart validation & logic DefaultCommerceAddToCartStrategy
FindPriceStrategy Price resolution Europe1FindPriceStrategy
FindDiscountValuesStrategy Discount resolution Europe1FindDiscountValuesStrategy
DeliveryModeLookupStrategy Available delivery modes DefaultDeliveryModeLookupStrategy
CommerceStockLevelCalculationStrategy Stock level calculation DefaultCommerceStockLevelCalculationStrategy

To customize pricing logic, you'd implement FindPriceStrategy and register it in Spring:

<bean id="findPriceStrategy" class="com.mycompany.strategies.CustomFindPriceStrategy"/>
Enter fullscreen mode Exit fullscreen mode

The platform resolves strategies by bean ID convention or via explicit injection.

Interceptor Chain

Interceptors are lifecycle hooks on ModelService operations. When you call modelService.save(model), the platform executes a chain of interceptors before and after persistence.

The interceptor types, in execution order:

  1. InitDefaultsInterceptor — Called during modelService.create(). Sets default values.
  2. PrepareInterceptor — Called before save. Transforms data (e.g., generate a code, compute a derived field).
  3. ValidateInterceptor — Called before save, after prepare. Validates business rules. Throws InterceptorException to abort.
  4. RemoveInterceptor — Called before modelService.remove(). Can prevent deletion or clean up related data.
  5. LoadInterceptor — Called when a model is loaded from the database. Rarely used.
public class LoyaltyTransactionPrepareInterceptor implements PrepareInterceptor<LoyaltyTransactionModel> {

    @Override
    public void onPrepare(LoyaltyTransactionModel model, InterceptorContext ctx) throws InterceptorException {
        if (model.getCode() == null) {
            model.setCode("LT-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration in Spring:

<bean id="loyaltyTransactionPrepareInterceptor"
      class="com.mycompany.interceptors.LoyaltyTransactionPrepareInterceptor"/>

<bean id="loyaltyTransactionPrepareInterceptorMapping"
      class="de.hybris.platform.servicelayer.interceptor.impl.InterceptorMapping">
    <property name="interceptor" ref="loyaltyTransactionPrepareInterceptor"/>
    <property name="typeCode" value="LoyaltyTransaction"/>
</bean>
Enter fullscreen mode Exit fullscreen mode

Important internal detail: The InterceptorRegistry class maintains mappings from type codes to interceptor lists. The DefaultModelService iterates through InterceptorExecutionPolicy to execute the chain. You can disable interceptors for bulk operations:

Map<String, Object> context = new HashMap<>();
context.put(InterceptorExecutionPolicy.DISABLED_INTERCEPTOR_BEANS, 
    Set.of("loyaltyTransactionPrepareInterceptor"));
modelService.save(model, context);
Enter fullscreen mode Exit fullscreen mode

Event System

SAP Commerce has an internal event system based on AbstractEvent and AbstractEventListener:

// Custom event
public class LoyaltyPointsEarnedEvent extends AbstractEvent {
    private final String customerUid;
    private final int points;

    public LoyaltyPointsEarnedEvent(String customerUid, int points) {
        this.customerUid = customerUid;
        this.points = points;
    }
    // getters...
}

// Listener
public class LoyaltyPointsEarnedListener extends AbstractEventListener<LoyaltyPointsEarnedEvent> {
    @Override
    protected void onEvent(LoyaltyPointsEarnedEvent event) {
        // Send notification, update analytics, etc.
    }
}

// Publishing
eventService.publishEvent(new LoyaltyPointsEarnedEvent("john.doe", 100));
Enter fullscreen mode Exit fullscreen mode

Events can be cluster-aware (broadcast to all nodes) by extending ClusterAwareEvent and implementing publish(int sourceNodeId).

When to Customize vs. Extend vs. Use OOTB

Approach When to Use Risk Level
OOTB (Out of the Box) Feature matches requirements 80%+ Low
Configuration Behavior can be adjusted via properties/ImpEx Low
Extend (Add Populator/Strategy/Interceptor) Need additional behavior without changing existing code Medium
Override (Replace Bean) Need to change existing behavior Medium-High
Custom Extension New domain/feature not covered by OOTB High
Modify OOTB Code NEVER — breaks upgrades Critical

Golden Rule: Never modify files in bin/platform/ or bin/modules/. Always customize through your own extensions using Spring bean overriding, type system extension, and the patterns described above.

Top comments (0)