SapotaCorp

Models and ViewData in SFRA: The Clean Data Flow

B2C Commerce SFRA separates the platform API from the template via a model layer. Done right, the templates render cleanly and the controllers stay thin. Done wrong, templates query the API directly and the codebase fragments.

Models and ViewData in SFRA: The Clean Data Flow

Key takeaways

  • SFRA's model layer separates platform API (dw.* objects) from templates (ISML files). The data flow runs controller -> factory -> model -> ViewData -> template. Done right, controllers stay thin and templates render cleanly. Done wrong, templates query the API directly and the codebase fragments.
  • Factory pattern is the entry point. ProductFactory.get(params) returns a fully-shaped model object; the factory decides which model variant fits (Standard, Bundle, Set, Variation). The factory pattern is one of SFRA's key improvements over SiteGenesis where the same dispatch was scattered across pipelines.
  • Model decorators compose. The base model exposes core fields; each cartridge can decorate the model with additional fields via require/extend. The decoration pattern keeps the cartridge path's customisation composition working — each cartridge adds its data without overwriting siblings.
  • ViewData (res.viewData) is the template's data contract. Every field the template needs must be on ViewData; templates should never reach back to the platform API. Code review catches direct API calls in ISML as the most common SFRA anti-pattern.

B2C Commerce SFRA separates the platform API (the dw.* objects) from the template (the ISML files) via a model layer. Done right, the controller is thin, the model handles the shape, and the template renders cleanly. Done wrong, templates query the API directly, business logic spreads across templates and controllers, and the codebase fragments.

The model pattern is one of SFRA's key improvements over SiteGenesis. Understanding it is the difference between code that ships clean and code that produces maintenance issues for years.

The data flow

A standard SFRA page flow:

  1. Request arrives at the controller. The controller knows the route (e.g., Product-Show).
  2. Controller calls a factory to get a model. ProductFactory.get({ pid: req.querystring.pid }) returns a fully-shaped model object.
  3. Model decorates the platform API. Internally, the model reads dw.catalog.ProductMgr.getProduct(pid) and shapes the data into a template-friendly structure.
  4. Controller passes the model to res.render. The template receives the shaped object via pdict.
  5. Template renders. Uses the model's properties directly, no API calls inside ISML.

The contract: controllers and models touch the platform API; templates do not.

The factory pattern

A factory is a function that returns a model. The factory hides the construction logic and any caching or decoration. A typical factory:

// scripts/factories/product.js
'use strict';

var ProductMgr = require('dw/catalog/ProductMgr');
var FullProduct = require('*/cartridge/models/product/fullProduct');
var ProductTile = require('*/cartridge/models/product/productTile');

function get(params) {
  var apiProduct = ProductMgr.getProduct(params.pid);
  if (!apiProduct) return null;

  if (params.variant === 'tile') {
    return new ProductTile(apiProduct, params);
  }

  return new FullProduct(apiProduct, params);
}

module.exports = { get: get };

The factory chooses which model class to instantiate based on the context. A product detail page wants the full model with variants, images, descriptions, related products. A product tile in a category list wants a lighter model with just the data needed for the tile.

Multiple model shapes per object type is normal. The factory routes; the models specialize.

Model decorator pattern

SFRA's base models use a decorator pattern: each model has a base class with a set of decorators (functions that add specific properties). A typical model:

// models/product/fullProduct.js
'use strict';

var ProductBase = require('*/cartridge/models/product/productBase');
var decorators = require('*/cartridge/models/product/decorators/index');

function FullProductModel(product, productVariables, productType, options) {
  ProductBase.call(this, product, productVariables, productType, options);

  decorators.price(this, product);
  decorators.images(this, product, { types: ['large', 'small'], quantity: 'all' });
  decorators.description(this, product);
  decorators.ratings(this);
  decorators.promotions(this);
  decorators.attributes(this, product);
  decorators.options(this, product, options);
  decorators.relatedProducts(this, product);

  return this;
}

module.exports = FullProductModel;

Each decorator adds a specific property family. The model composes them. Customizations override or extend individual decorators without touching the model's structure.

This is the pattern that makes SFRA upgradeable. Salesforce can add a new decorator (decorators.newFeature) to base models in a future release. Your customizations sit alongside, unaffected.

Customizing models in your cartridge

To customize a model, override its decorator in your custom cartridge:

// custom_cartridge/cartridge/models/product/decorators/price.js
'use strict';

var basePrice = module.superModule;

module.exports = function (object, product, defaultProduct, options) {
  basePrice.call(null, object, product, defaultProduct, options);

  // Add custom pricing logic.
  object.price.customDiscount = computeCustomDiscount(product);
};

Three things to know:

1. module.superModule references the same file from the cartridge below in the path. Available because SFRA's require system resolves modules through the cartridge path.

2. Call the base decorator first. Then layer custom behavior. This pattern preserves base functionality while extending.

3. Mutate the object passed in, do not replace it. The decorator pattern works on a shared object reference. Replacing would break the chain.

ViewData as the controller-template contract

Once the controller has the model, it passes it to the template via res.render. Internally, this populates pdict in the template:

// Controller
res.render('product/productDetails', {
  product: productModel,
  resources: { /* localized strings */ },
  canonicalUrl: '...'
});
<!-- Template -->
<h1><isprint value="${pdict.product.productName}"/></h1>
<p><isprint value="${pdict.product.shortDescription}"/></p>

The template reads from pdict.product.*. No dw.catalog.ProductMgr.getProduct in the template. The model has done the work; the template renders.

ViewData is also mutable across middleware chains. A controller that adds an analytics tag in an append:

server.append('Show', function (req, res, next) {
  var viewData = res.getViewData();
  viewData.analyticsTag = computeTag(viewData.product);
  res.setViewData(viewData);
  next();
});

The base controller set viewData.product. The append reads it, computes additional data, writes it back. The template sees the merged result.

When to introduce a new model

Three signs you need a new model:

1. The template needs a transformed version of an existing model. A product tile in a category page needs subset of full product data plus a few category-specific computed fields. Build a ProductTile model.

2. The data shape involves multiple platform objects. A "Cart with customer context" object combining dw.order.BasketMgr.getCurrentBasket() with dw.customer.CustomerMgr.getCurrentCustomer(). Build a model that composes both.

3. The same shape appears in three or more templates. Stop copying the data preparation logic across controllers. Move it into a model.

When in doubt, build a model. The cost is one file. The benefit is consistent shape across consumers.

Common model and ViewData mistakes

Five patterns Sapota has seen on engagements:

1. API calls in templates. ISML calling dw.catalog.ProductMgr.getProduct() directly. Breaks the layering, makes caching harder, mixes presentation with data access. Move to model.

2. Models with platform-API leaks. A "model" that exposes raw dw.* objects to the template. The template ends up calling .getCustom().getValue() chains directly. Encapsulate fully; expose only template-friendly properties.

3. Decorators that replace base decorators. Custom price decorator replaces the entire base decorator instead of extending. Loses base behavior. Use module.superModule.call() to preserve.

4. Fat controllers doing the model's work. Controller iterates product variants, computes display strings, joins with related data. All of this belongs in a model. Move it.

5. ViewData mutation without setViewData. An append modifies res.viewData.product.name = '...'. The mutation does not persist correctly across the chain. Always read via getViewData, mutate, write via setViewData.

What good model architecture looks like

A B2C Commerce SFRA site with healthy models:

  • Factories route requests to appropriate model variants.
  • Models compose via decorators, customizations extend via override + superModule.
  • Templates read only from pdict, no API calls.
  • Controllers stay under 100 lines.
  • ViewData mutation patterns consistent (getViewData / setViewData).
  • Test coverage on models exercising decorators independently.

The model layer is the part of SFRA that takes the longest to internalize and pays back the most over time. A site that respects the layering ships features faster and survives platform upgrades cleanly. Sapota's Salesforce team treats model architecture as a design deliverable on every B2C Commerce engagement.


Designing or refactoring the model layer in a B2C Commerce site? Sapota's Salesforce team, certified on B2C Commerce Developer (Comm-Dev-101), handles model architecture, decorator design, and ViewData patterns on production engagements. Get in touch ->

See our full platform services for the stack we cover.

Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project