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:
The
commerceWebServicesSessionFilterintercepts the request. This Spring filter runs before the controller. It reads the cart ID from the URL and the user token from the header.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 session –
session.setAttribute("cart", cart). It also sets the current user, the base site, the catalog version, the language, and the currency into session-scoped attributes.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.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.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 cart –
cartService.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:
- Set delivery address → saved to session cart
- Set delivery mode → saved to session cart
- Set payment info → saved to session cart
- Review order → read from session cart
- 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:
- Create HTTP session object (memory allocation)
- Load cart from database (DB query)
- Load user context (DB query or cache lookup)
- Set site, catalog, currency, language in session (multiple attribute sets)
- Execute the actual business logic
- Discard the session (GC pressure)
Per-request overhead in a truly stateless service:
- Parse cart ID from URL
- Load cart from database (DB query – same as above)
- 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:
| Aspect | OCC | Stateless Design |
|---|---|---|
| Cart access | cartService.getSessionCart() (reads from HTTP session) | cartRepository.findById(cartId) (reads from DB by parameter) |
| User context | Read from session, set by filter | @AuthenticationPrincipal from JWT – no session |
| Price resolution | PriceService reads currency/group from session | priceService.resolve(product, priceGroup, currency) – explicit parameters |
| Session object | Created per request, hydrated, discarded | Does not exist |
| Filter chain | commerceWebServicesSessionFilter does DB read + session hydration | No filter needed |
| Concurrency safety | Last-write-wins on parallel requests | @Transactional with optimistic locking on cart version |
| Missing context | Silent fallback to defaults | Compile 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, catalogCartService.getSessionCart()– used by every cart-related facadeUserService.getCurrentUser()– reads from session-scoped contextBaseSiteService.getCurrentBaseSite()– session attributeCatalogVersionService.getSessionCatalogVersions()– session attributeI18NService.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.