SAP Commerce Cloud (Hybris) offers an API layer called OCC – Omni Commerce Connect. It’s marketed as a RESTful, stateless API for building headless storefronts, mobile apps, and single-page applications. It’s the API that powers Spartacus, SAP’s reference Angular storefront.

The API endpoints look right. POST /occ/v2/{site}/carts/{cartId}/entries to add an item to a cart. GET /occ/v2/{site}/products/{productCode} to fetch a product. Cart IDs in the URL. OAuth tokens in the header. JSON in, JSON out. Textbook REST.

Except it isn’t stateless. Underneath the REST surface, every OCC request creates an HTTP session, loads the cart into it, hydrates user context, site configuration, and currency settings – then routes the request through the same session-dependent facade layer that was built for the old JSP Accelerator storefront.

OCC is not a stateless API. It is a REST facade bolted onto a stateful monolith.

What Actually Happens on Every Request

When a client calls POST /occ/v2/mysite/carts/12345/entries, the request doesn’t go directly to a stateless controller that loads cart 12345 from the database, adds an item, saves, and returns. Instead:

  1. The commerceWebServicesSessionFilter intercepts the request. This Spring filter runs before the controller. It reads the cart ID from the URL and the user token from the header.

  2. The filter creates an HTTP session (or reuses one if sticky sessions are configured). It loads the cart from the database and puts it into the HTTP sessionsession.setAttribute("cart", cart). It also sets the current user, the base site, the catalog version, the language, and the currency into session-scoped attributes.

  3. The OCC controller calls the facade. CartFacade.addToCart(productCode, quantity) – the same facade used by the old Accelerator JSP storefront. The facade signature takes no cart parameter. It doesn’t need one – it reads the cart from the session.

  4. The facade calls cartService.getSessionCart(). This method reaches into the HTTP session, retrieves the cart object, and returns it. The service adds the item, recalculates totals, and writes the modified cart back to the session.

  5. The response is serialized and returned to the client. The session is discarded (in stateless mode) or kept alive (in sticky-session mode).

The client thinks it’s talking to a stateless API. The server is running the same session-scoped code it has always run. The commerceWebServicesSessionFilter is the glue – it translates between the REST world (cart ID in URL) and the session world (cart in HttpSession).

The Cart: The Most Obvious Example

The cart is where the session dependency is most visible. Consider the add-to-cart flow:

What a stateless implementation would do:

1. Parse cart ID from URL path
2. Load cart from database by ID
3. Add item to cart
4. Save cart to database
5. Return updated cart as JSON

No session. No filter. The cart travels through the code as a function parameter.

What OCC actually does:

1. commerceWebServicesSessionFilter intercepts request
2. Filter reads cart ID from URL
3. Filter loads cart from database
4. Filter puts cart into HTTP session
5. Filter sets user/site/currency in session
6. Controller calls CartFacade.addToCart(productCode, qty)
7. CartFacade calls cartService.getSessionCart()
8. cartService reads cart from HTTP session
9. Item is added to the session cart
10. Modified cart is saved to database
11. Controller reads result and returns JSON

Steps 2-5 exist solely to satisfy the facade’s assumption that a session cart exists. The filter does a database read, a session write, and context hydration – on every single API call – just to set up the environment that the facade expects.

The facade was written for the Accelerator storefront, where the user had a persistent browser session. The cart was loaded once at login and lived in the session for the entire shopping experience. It was never designed to be loaded from scratch on every request. OCC forces it to do exactly that.

Promotions: Session-Dependent Calculation

The promotion engine is one of the most complex subsystems in Hybris. It evaluates rules (buy X get Y free, spend $100 get 10% off, etc.) against the cart and applies discounts.

The promotion engine reads:

  • The session cartcartService.getSessionCart() to get the cart being evaluated
  • The session user – to determine which customer group promotions apply
  • The session site – to scope promotions to the correct storefront
  • Cached promotion results – stored in the session to avoid re-evaluation on every page load

For the Accelerator storefront, this made sense. The user was in a session. The cart was in the session. Promotion results were cached in the session for the duration of the shopping experience.

For OCC, the commerceWebServicesSessionFilter must hydrate all of this context from scratch on every request. The promotion cache in the session is useless – the session is created, used once, and discarded. The “optimization” of caching promotion results in the session becomes pure overhead in the OCC world: allocate session, populate cache, evaluate promotions, serialize response, discard session (and its cache).

Worse: if the session hydration order changes (e.g., promotions are evaluated before the currency is set in the session), the promotion engine silently uses default values. The promotions might evaluate correctly for one currency and incorrectly for another – depending on which session attributes the filter happened to set first.

Pricing: Silent Fallbacks from Missing Session Context

Price resolution in Hybris depends on session context:

  • User price group – B2B vs B2C, loyalty tier, contract pricing
  • Currency – session currency determines which price row is selected
  • Catalog version – staged vs online catalog, which price catalog is active
  • Date/time – time-based pricing (handled via session-scoped date context)

The PriceService reads these from the session. It does not accept them as method parameters. When the commerceWebServicesSessionFilter sets up the session for an OCC request, it must correctly populate every one of these attributes. If it misses one – if the currency isn’t set, or the user’s price group isn’t loaded – the price service does not throw an error. It silently falls back to the default price row.

This means a misconfigured OCC filter can return wrong prices without any error, any log message, or any indication that something went wrong. The API returns 200 OK with a price that doesn’t match what the customer should see. This is the same class of problem as the “return 200 for everything” anti-pattern – the system lies about success.

In a properly stateless design, the price service would accept currency, user group, and catalog version as explicit parameters. A missing parameter would be a compile error or a runtime validation error – not a silent fallback to a default.

Checkout: The Stateful Wizard Behind REST Endpoints

The checkout flow in Hybris was designed as a multi-step wizard:

  1. Set delivery address → saved to session cart
  2. Set delivery mode → saved to session cart
  3. Set payment info → saved to session cart
  4. Review order → read from session cart
  5. Place order → read everything from session cart, create order

Each step reads the session cart, modifies it, and writes it back. The next step assumes the previous step’s changes are already in the session.

OCC exposes these as separate REST endpoints:

PUT /carts/{id}/addresses/delivery       (step 1)
PUT /carts/{id}/deliverymode             (step 2)
PUT /carts/{id}/paymentdetails           (step 3)
POST /orders                             (step 5)

Each of these endpoints hits the commerceWebServicesSessionFilter, which creates a fresh session, loads the cart, hydrates context, calls the facade, and discards the session. The cart’s state is persisted to the database between calls, so the flow works – but it works by accident of persistence, not by design.

The real problem: concurrency. If two requests arrive simultaneously for the same cart (e.g., set delivery address and set payment info in parallel), both requests load the cart into separate sessions, both modify their respective fields, and both save. The last save wins. One change is silently lost.

In the Accelerator storefront, this was never a problem – the user clicked through a wizard sequentially, and the session serialized access. In a headless SPA making parallel API calls, it’s a data integrity risk.

The Scaling Problem

The promise of a stateless API is horizontal scalability: any server can handle any request, no affinity required, no shared session state.

OCC technically achieves this – any server can handle any request, because the commerceWebServicesSessionFilter loads everything from the database on every request. But the cost is significant:

Per-request overhead in OCC:

  1. Create HTTP session object (memory allocation)
  2. Load cart from database (DB query)
  3. Load user context (DB query or cache lookup)
  4. Set site, catalog, currency, language in session (multiple attribute sets)
  5. Execute the actual business logic
  6. Discard the session (GC pressure)

Per-request overhead in a truly stateless service:

  1. Parse cart ID from URL
  2. Load cart from database (DB query – same as above)
  3. Execute the actual business logic

Steps 1, 3-4, and 6 in the OCC flow are pure overhead – they exist only to satisfy the session-dependent facade layer underneath. The database query to load the cart is necessary in both designs, but OCC wraps it in session machinery that adds memory allocation, context hydration, and garbage collection for no functional benefit.

Under high load (Black Friday, flash sales), this overhead multiplies. Every concurrent request creates its own session object, hydrates its own context, and discards it. The garbage collector works harder. The session filter becomes a serialization point if it uses any shared state (like the session service’s internal locks).

What a Properly Stateless Design Looks Like

Here’s the same add-to-cart operation, designed from scratch as a stateless service:

@RestController
@RequestMapping("/api/carts")
public class CartController {

    @PostMapping("/{cartId}/entries")
    @ResponseStatus(HttpStatus.CREATED)
    public CartResponse addToCart(
            @PathVariable UUID cartId,
            @Valid @RequestBody AddToCartRequest request,
            @AuthenticationPrincipal JwtUser user) {

        return cartService.addItem(cartId, request.getProductCode(),
                                   request.getQuantity(), user);
    }
}
@Service
public class CartService {

    @Transactional
    public CartResponse addItem(UUID cartId, String productCode,
                                int quantity, JwtUser user) {
        Cart cart = cartRepository.findByIdAndUserId(cartId, user.getId())
                .orElseThrow(() -> new NotFoundException("Cart not found"));

        Product product = productRepository.findByCode(productCode)
                .orElseThrow(() -> new NotFoundException("Product not found"));

        cart.addEntry(product, quantity);

        BigDecimal price = priceService.resolve(product, user.getPriceGroup(),
                                                 user.getCurrency());
        cart.recalculate(price);

        cartRepository.save(cart);
        return CartMapper.toResponse(cart);
    }
}

Notice what’s different:

AspectOCCStateless Design
Cart accesscartService.getSessionCart() (reads from HTTP session)cartRepository.findById(cartId) (reads from DB by parameter)
User contextRead from session, set by filter@AuthenticationPrincipal from JWT – no session
Price resolutionPriceService reads currency/group from sessionpriceService.resolve(product, priceGroup, currency) – explicit parameters
Session objectCreated per request, hydrated, discardedDoes not exist
Filter chaincommerceWebServicesSessionFilter does DB read + session hydrationNo filter needed
Concurrency safetyLast-write-wins on parallel requests@Transactional with optimistic locking on cart version
Missing contextSilent fallback to defaultsCompile error (missing parameter) or validation error

The stateless design has no session, no filter, no implicit context. Every dependency is an explicit parameter. Missing context is a compile error, not a silent default. Concurrency is handled by the database (optimistic locking), not by hoping requests don’t overlap.

Why SAP Didn’t Fix It

The honest answer: rewriting the facade and service layer to be truly stateless would mean rewriting the entire Hybris commerce engine.

The session dependency is not limited to a few convenience methods. It’s woven through hundreds of services:

  • SessionService.getAttribute() – called across cart, pricing, promotions, CMS, search, catalog
  • CartService.getSessionCart() – used by every cart-related facade
  • UserService.getCurrentUser() – reads from session-scoped context
  • BaseSiteService.getCurrentBaseSite() – session attribute
  • CatalogVersionService.getSessionCatalogVersions() – session attribute
  • I18NService.getCurrentCurrency() / getCurrentLanguage() – session attributes

Every one of these methods assumes a session exists and has been populated. Refactoring them to accept explicit parameters would touch thousands of classes across dozens of extensions. It would break every custom extension built by every SAP customer and partner.

The commerceWebServicesSessionFilter was the pragmatic shortcut: keep the stateful engine, bolt a REST skin on top, and use a filter to translate between the two worlds. It works. It scales well enough for most production loads. But it is not what it claims to be.

Key Takeaway

OCC is a leaky abstraction. The REST contract says stateless. The implementation says stateful. The filter is the seam where the two worlds meet, and it’s where the architecture’s compromises are most visible.

This matters because developers building on OCC make assumptions based on the REST contract:

  • “I can make parallel requests” → You can, but concurrent writes to the same cart may lose data.
  • “Any server can handle any request” → True, but every request pays the session hydration tax.
  • “Prices and promotions are deterministic for the same input” → They are, unless the session context is hydrated in a different order.
  • “Missing parameters will cause errors” → They won’t. They’ll cause silent fallbacks.

The lesson is broader than Hybris: wrapping a stateful system in a REST API does not make it stateless. If the implementation depends on session state, the API inherits those dependencies – regardless of what the endpoint URLs look like. True statelessness is an implementation property, not a contract property.