Sharing Product Data Between AEM Components: From Session Pattern to Cache

Sharing Product Data Between AEM Components: From Session Pattern to Cache

The Context

In an Adobe Experience Manager e-commerce project, we faced a significant architectural evolution: the product page evolved from a single monolithic component to N specialized components.

The Problem

  • Before: A single component loaded all product information from the backend
  • After: Multiple components (header, gallery, specs, reviews, cross-sell, etc.) needed the same product information
  • Critical issues: Calling the backend N times for each component would have caused:
    • Degraded performance
    • Rendering timeouts
    • High costs on backend APIs

Key question: How can all components access the same product data with only one backend call?


Solution 1: Session Pattern

Our first solution leveraged a fundamental AEM characteristic: every page has a session and HTL processing is sequential, not parallel.

Strategy

  1. Place a Sling Model in the page header
  2. On load, the model makes a single backend call (via Service)
  3. Save the product object in the page session
  4. Other page components read from the session

Why does it work?

HTL processing in AEM is top-down and sequential:

Header (executes first)
  ↓ saves to session
Body Component 1 (reads from session)
  ↓
Body Component 2 (reads from session)
  ↓
Body Component N (reads from session)

Technical Implementation

1. Header Model - Loading and saving to session

@Model(adaptables = SlingHttpServletRequest.class)
public class ProductPageHeaderModel {

    private static final String SESSION_PRODUCT_KEY = "current.product.data";

    @SlingObject
    private SlingHttpServletRequest request;

    @OSGiService
    private ProductService productService;

    @PostConstruct
    protected void init() {
        // Extract product ID from URL or page properties
        String productId = getProductIdFromPage();

        if (productId != null) {
            // Call backend ONCE
            Product product = productService.getProduct(productId);

            // Save to session to share with other components
            request.getSession().setAttribute(SESSION_PRODUCT_KEY, product);
        }
    }

    public Product getProduct() {
        return (Product) request.getSession().getAttribute(SESSION_PRODUCT_KEY);
    }

    private String getProductIdFromPage() {
        // Logic to extract product ID
        // (from URL selectors, page properties, etc.)
        return request.getRequestPathInfo().getSelectorString();
    }
}

2. Service - Backend call

The ProductService is a simple OSGi service that handles calling the e-commerce backend (REST/GraphQL) and returning the parsed Product object.

@Component(service = ProductService.class)
public class ProductServiceImpl implements ProductService {

    @Override
    public Product getProduct(String productId) {
        // Call e-commerce backend and parse response
        // (implementation omitted for brevity)
    }
}

3. Body Components - Reading from session

@Model(adaptables = SlingHttpServletRequest.class)
public class ProductSpecsModel {

    private static final String SESSION_PRODUCT_KEY = "current.product.data";

    @SlingObject
    private SlingHttpServletRequest request;

    public Product getProduct() {
        // Read from session (already populated by header)
        return (Product) request.getSession().getAttribute(SESSION_PRODUCT_KEY);
    }

    public List<Specification> getSpecs() {
        Product product = getProduct();
        return product != null ? product.getSpecifications() : Collections.emptyList();
    }
}

Session Pattern Advantages

Single backend call per page ✅ Simple to implement - uses standard Sling APIs ✅ Guaranteed sequentiality - HTL processes top-down ✅ No external dependencies - only native AEM session

Disadvantages

Session bound to request - doesn't persist between requests ❌ Memory overhead (minimal, but present) ❌ Doesn't work with async rendering (if introduced in the future)


Evolution: Cache Layer

Subsequently, to further optimize performance, we introduced a cache layer at the Service level.

Cache Implementation

@Component(service = ProductService.class)
public class ProductServiceImpl implements ProductService {

    @Reference
    private CacheManager cacheManager;

    @Override
    public Product getProduct(String productId) {
        // 1. Check cache
        Product cached = cacheManager.get("products", productId);
        if (cached != null) {
            log.debug("Product {} served from cache", productId);
            return cached;
        }

        // 2. Cache miss - call backend
        Product product = fetchFromBackend(productId);

        // 3. Save to cache (TTL: 5 minutes)
        if (product != null) {
            cacheManager.put("products", productId, product, 300);
        }

        return product;
    }
}

Cache Impact on Session Pattern

With cache enabled, the Session Pattern becomes almost superfluous:

Scenario Without Cache With Cache
First page request Backend call → Session Backend call → Cache + Session
Second request (same user) Backend call → Session Cache hit (no backend)
Different user request Backend call → Session Cache hit (no backend)

Result: The session becomes just an "intermediate step" because cache solves the problem at the root.


Lessons Learned

When to use the Session Pattern?

Good for:

  • Sharing data between components in the same request
  • Projects without advanced cache
  • Frequently changing data (no cache)
  • Rapid prototyping

Avoid if:

  • You already have an efficient cache layer (becomes redundant)
  • You use client-side async rendering
  • Components can be reused on other pages

Final Best Practice

In our case, the optimal solution was:

  1. Cache at Service level (TTL 5-10 min)

    • Drastically reduces backend calls
    • Shared among all users
  2. Session Pattern as fallback (optional)

    • Useful if cache is disabled (e.g., dev environment)
    • Still guarantees one call per request
  3. Smart cache invalidation

    • Invalidate cache when product is updated in backend
    • JCR/Workflow events for automatic invalidation

Conclusions

The Session Pattern is an elegant and simple solution for sharing data between AEM components on the same page, leveraging the sequential nature of HTL rendering.

However, in high-performance scenarios like e-commerce, adding a cache layer becomes the true scalable solution, making the session a secondary or fallback mechanism.

Key Takeaways

  1. HTL processes components sequentially from top to bottom
  2. AEM session can be used to share intra-request data
  3. A well-designed cache outperforms Session Pattern in performance
  4. The best solution is often a mix of both (cache + session fallback)

Have you implemented similar patterns in your AEM projects? Share your experience in the comments or contact me to discuss!

Related articles: