DEV Community

Arkadiusz Graczyk
Arkadiusz Graczyk

Posted on

From Spartacus to Composable Storefront: What Really Changed Around NgRx?

Starting with version 5.0, SAP Commerce Cloud, composable storefront became SAP’s officially supported offering based on open-source Spartacus. The name changed; the architecture did not — not on that day.

NgRx was not removed. It is still part of the core architecture. What changed was how much new code needed to depend on it directly, especially for state originating from APIs.

In the pinned commit used here, @spartacus/core is version 221121.17.0. Its manifest still lists @ngrx/store, @ngrx/effects and @ngrx/router-store as peer dependencies. The current architecture guide still describes backend data as being held in a central NgRx store and exposed through facades.

What stayed

The documented data flow is:

Component → Facade → Store / Effects → Connector → Adapter → OCC

Facades keep UI code away from NgRx. Effects coordinate state changes and backend operations. Connectors delegate calls to adapters. These layers gave teams clear places to customise behaviour, but they also tied core flows closely to NgRx.

What changed

Spartacus 3.2 introduced proxy facades, so application code could depend on lightweight contracts while their implementations lived in lazy-loaded features.

The core-3.2.0 source already contains CommandService and QueryService. Adoption came later, feature by feature. The public guide now sits in the archived 4.x documentation, which makes the mechanism look newer than the tagged source. Checkout moved to Commands and Queries in a later migration. The code, documentation and adoption did not land together.

Commands and Queries handle backend-driven loading, caching and errors without requiring a full action, reducer, selector and effect chain. A Query can stay behind a facade method:

@Injectable()
export class UserProfileService implements UserProfileFacade {
  protected query = inject(QueryService);
  protected userProfileConnector = inject(UserProfileConnector);

  protected titleQuery = this.query.create(
    () => this.userProfileConnector.getTitles()
  );

  getTitles(): Observable<Title[]> {
    return this.titleQuery.get();
  }
}
Enter fullscreen mode Exit fullscreen mode

Commands and Queries do not replace facades. The same facade contract can be backed by Store and selectors in one feature and by a Query or Command in another.

On one long-lived storefront I worked with, an older saved-cart feature still used the full action–effect–reducer–selector chain, while newer checkout code used Commands and Queries behind facades. Both worked. Rewriting the older flow would have added migration risk without changing what the customer saw. The facade was the useful boundary: new code could evolve without forcing the older state model to move with it.

Before adding another state flow

In a long-lived storefront, the cost of one more action, effect or selector is not the file itself. It is another path to trace while debugging and another customisation to carry through upgrades.

Before adding global state, ask:

  1. Is there already a facade that exposes the behaviour I need?
  2. Is this genuinely shared application state, or backend data with a lifecycle?
  3. Can the customisation remain behind a connector or adapter instead of spreading into the UI and global store?

Top comments (0)