Performance and scalability

Elevate, May 20-22-2025, Miami Beach, Florida

Learn about performance and scalability considerations when using Composable Commerce Categories to implement navigation.

After completing this page, you should be able to:

  • Evaluate and implement performance optimization and caching strategies when building Category navigation with Composable Commerce.

Building on our previous section on querying Composable Commerce Categories for a mega menu, this section dives into critical performance and scalability considerations. A fast and responsive Category navigation experience, like a mega menu, relies heavily on efficient data retrieval from Composable Commerce. Let's explore the common pitfalls and best practices.

The challenge: inefficient data fetching with offset pagination

A common initial approach to fetching data in pages is using offset and limit parameters. While simple to understand, offset-based pagination doesn't scale well, especially when dealing with a large number of Categories.

Why offset pagination hinders performance

The core performance issue is that the platform must scan results from the very beginning every time you fetch a page using offset. For example, if you request limit=10&offset=100, the system internally processes the first 110 items just to return the 10 you need. As the offset value increases (meaning you're requesting pages further down the list), the query time increases significantly because more and more data needs to be scanned internally before your requested page can be returned.

Platform limits and when to avoid offset

Composable Commerce enforces a practical limit of 10 000 items on the offset parameter. This means you cannot reliably retrieve items beyond this limit using this method. The total count returned in the response is also effectively limited by this internal scanning mechanism. Due to these performance implications and limits, offset pagination should be avoided for fetching large datasets or implementing features requiring deep navigation.

The solution: efficient fetching with cursor-based pagination

To overcome the performance challenges of offset pagination and ensure your Category navigation scales effectively, the recommended approach is cursor-based pagination.

Cursors provide a stable reference point (like a bookmark) within your sorted dataset. Instead of telling the system to "skip X items", you tell it to "start fetching items after this specific reference point". This allows the platform to fetch the next set of results directly, avoiding the costly scanning associated with high offsets.

In Composable Commerce, you typically implement cursor-based pagination using a where predicate combined with sort. The cursor itself is usually a value from the last item retrieved on the previous page.

Choosing a cursor field

While a resource's id is a common choice for the cursor due to its uniqueness and inherent sortability, other fields like createdAt or lastModifiedAt can also serve effectively. Generally, any field with highly unique values that is lexicographically sortable makes a suitable cursor. You sort your query by this field and use its value from the last item of a page in the where clause for the next page (for example, where=id > "last-retrieved-id").

Best practice: deactivate withTotal for performance

Another important performance optimization, regardless of the pagination method, is to deactivate the total count calculation by setting withTotal=false in your queries. Requesting the total count (withTotal=true) forces Composable Commerce to count every single matching document, which can be slow, similar to the scanning issue with offset pagination. It's generally more efficient to retrieve data page by page using cursors without calculating the overall total upfront.

Implement a Caching Strategy

Category trees, often required on every page load, exemplify data with high read volume and infrequent changes (low write volume). Updates typically only occur when Category details are modified or new Categories are added. This read-heavy, write-light pattern makes caching an ideal strategy to reduce response times and server load, significantly improving application performance and scalability.

While Category trees are a prime example, caching various types of application data (for example, product details, pricing, inventory) fetched from backend systems is vital for optimizing overall performance and user experience. However, since application data can change, implementing effective cache invalidation strategies is crucial to prevent serving stale information.

When designing your caching approach, carefully consider your tolerance for eventually consistent data for each data type. Serving outdated prices or incorrect stock information can negatively impact user trust and experience, whereas serving a slightly stale Category mega menu might have a much lower impact. Effective strategies often involve multiple caching layers.

Common caching layers

Several caching layers can be employed. Two common server-side layers are the Backend-for-Frontend (BFF) and the Content Delivery Network (CDN). Client-side browser caching is another important layer discussed later under invalidation strategies.

FeatureBackend-for-Frontend (BFF)Content Delivery Network (CDN)
PurposeActs as middleware between frontend and backend; aggregates and processes data for the frontendCaches and serves static content or API responses from edge locations closer to users
Performance benefitReduces response times for repeated queries by minimizing direct calls to backend servicesReduces latency and offloads traffic from backend services, improving speed and scalability
Caching methodIn-memory cache (for example, Redis, Memcached) or application-level cache within the BFFEdge caching using CDNs (for example, Cloudflare, AWS CloudFront, Google Cloud CDN)
Implementation notesImplement caching logic in the BFF to store and serve frequently requested dataUse proper cache-control headers (max-age, s-maxage, stale-while-revalidate) to manage freshness

Cache invalidation strategies for application data

Cache invalidation strategies are essential to keep cached data reasonably up-to-date and ensure accuracy. Effective strategies often combine multiple techniques across different caching layers (CDNs, BFFs/application caches, and the user's browser).

Here are key strategies for managing cache freshness:

Time-based expiration (Time-To-Live & Stale-While-Revalidate)

This is a common foundational strategy. It involves setting a Time-To-Live (TTL), which is a fixed expiration time (for example, 60 minutes) on cached data. When the TTL expires, the cache considers the data stale and must fetch a fresh copy on the next request. This approach can be enhanced using Stale-While-Revalidate (SWR), which allows the cache to serve the slightly stale data immediately upon request while it asynchronously fetches an update in the background. SWR improves perceived performance by avoiding the latency typically associated with a cache miss during the refresh process.

  • Where it applies: CDNs, Reverse Proxies, BFFs, Application-level caches.
  • Implementation: configure the cache lifetime (max-age, s-maxage) balancing freshness needs with cache hit rates for the specific data type. Use stale-while-revalidate directives where appropriate.
  • Pros: simple to implement, provides a safety net ensuring data eventually refreshes. SWR minimizes latency during updates. Predictable cache behavior.
  • Cons: data can be stale for the duration of the TTL. A short TTL can reduce cache effectiveness, while a long TTL increases the staleness window. May not be suitable for highly volatile data like real-time inventory without very short TTLs.

Event-driven invalidation (near real-time)

For stronger consistency requirements where serving stale data is highly undesirable (for example, critical price, inventory updates, configuration data), use event-driven invalidation. This approach actively purges or updates specific cache entries almost immediately after a relevant change occurs in the source system.

  • Where it applies: primarily server-side caches (BFF, Application-level caches), but can trigger updates that influence downstream caches (like purging CDN paths).
  • Implementation:
    • Create a Subscription: set up a Composable Commerce Subscription targeting the relevant resource type (for example, product, inventory-entry, price). Configure it to send messages (for example, via AWS SQS, Google Pub/Sub, Azure Service Bus) upon object creation, update, or deletion events.
    The specific resourceTypeId and event types depend on the data you need to monitor.
    • Process Messages: deploy a small dedicated service that listens to the message queue. When a relevant message arrives (for example, indicating ProductPublished or InventoryEntryQuantitySet), this service triggers targeted invalidation actions (for example, purge request to CDN for specific product page paths, DELETE command to Redis cache for specific keys). Optionally, it can pre-warm the cache with the updated data, especially if the message contains all necessary data, avoiding extra backend queries.
  • Pros: provides near real-time data freshness, minimizing the staleness window significantly. Ideal for critical or rapidly changing data. Maximizes cache hit rates for unchanged data compared to relying solely on short TTLs. Can be used in conjunction with TTL/SWR as a fallback and for general expiry.
  • Cons: more complex to implement and maintain. Requires careful mapping between events and cache keys/paths for different data types. Potential for cache churn if frequently updated items are also frequently requested, which might negate performance benefits due to constant invalidation/repopulation overhead.

Browser/client-side caching

Leverage caching mechanisms available on the client-side (typically the user's web browser) to store data fetched via API calls (for example, Category listings, product details, user profile information). This reduces latency for repeat visits or navigation within the application, decreases load on your backend services, and improves perceived performance. This operates at two main levels:

  1. HTTP browser caching
The mechanism for this is controlled via standard HTTP caching headers sent from your server (BFF or API Gateway), such as Cache-Control (which includes directives like max-age, public, private, no-cache, no-store), ETag, and Last-Modified. The browser stores the raw HTTP response according to these directives and uses conditional requests (for example, If-None-Match with an ETag) to check for updates without re-downloading if the content hasn't changed.
  1. Framework-level / In-memory caching (high-level)

Modern JavaScript rendering frameworks (for example, React, Vue, Angular, Svelte) and their associated libraries often implement an additional layer of caching directly within the application's memory.

  • Mechanism: this typically involves:
    • State Management: Libraries like Redux, Zustand, Vuex, or Svelte Stores hold fetched data.
    • Data Fetching Libraries: Tools like React Query, SWR (the library), Apollo Client manage the lifecycle of fetched data, including caching, background updates, and invalidation within the JavaScript application.
  • Benefit: enables near-instant UI updates during navigation or re-renders by pulling data from the in-memory store without necessarily making a new HTTP request, even if the HTTP cache's max-age hasn't expired.
  • Invalidation: managed by the library's rules, often including time-based expiry (TTL), stale-while-revalidate logic, automatic refetching on window focus, or manual invalidation triggered by data mutations (for example, after a user updates their profile, invalidate the cached profile data).

Overall client-side strategy

Utilize HTTP Caching with validation headers (ETag) and appropriate Cache-Control directives as a baseline for network-level optimization. Leverage Framework-Level Caching capabilities (via state management or data-fetching libraries) to manage data within the application lifecycle for faster component rendering and smoother intra-app navigation. Ensure your API/BFF correctly generates validation headers (ETag, Last-Modified) based on the current state of the resource being served to support both caching layers effectively.

Considerations

Users can manually bypass the browser's HTTP cache (hard refresh). Framework cache behavior depends on the specific library and its configuration. Less granular server-side control exists compared to BFF/Application caches. Effectiveness relies heavily on correct server-side header implementation and thoughtful client-side state management.

Choosing the right combination

Often, a multi-layered approach tailored to different data types yields the best results. By carefully combining these strategies and tuning them for different types of application data, you can achieve a robust caching system that balances high performance with the required level of data freshness.

Test your knowledge