Friday, September 11, 2026
GitHubTwitter
GINBOK
HomeArticlesSearchAbout
|ENVIExplore
HomeArticlesSearchAbout
🇬🇧 English🇻🇳 Tiếng Việt
Articles›CMS & Content Platforms›Optimizely CMS: Find vs. Database Architectural Analysis
CMS & Content Platforms

Optimizely CMS: Find vs. Database Architectural Analysis

GinbokFeb 26, 20263 min read

In the lifecycle of developing Optimizely CMS applications, architects and developers frequently face a critical decision: "Should we use Optimizely (Episerver) Find or query the database directly via ContentLoader?" This is not merely a performance question; it is a strategic architectural decision affecting scalability, maintainability, and operational costs.

This article analyzes a real-world case study: optimizing ProductCatalogBlock queries in a multi-site e-commerce system with a relatively small footprint (approx. 25 products per category).

Case Study: Product Catalog Optimization

Project Context

We managed an Optimizely CMS multi-site system handling product catalogs for various brands. Each category utilized approximately 25 ProductCatalogBlock items to display product info on landing pages. Initially, the team implemented an Optimizely Find solution, assuming search-based retrieval was inherently superior for performance.

The Find-Based Solution

The original implementation leveraged the IClient interface to filter content indexed in the cloud:

public List<ProductCatalogBlock> GetProductsByCategoryId(int categoryId)
{
    try
    {
        var searchResults = _searchClient
            .Search<ProductCatalogBlock>()
            .Filter(x => x.CategoryId.Match(categoryId))
            .Filter(x => x.IsActive.Match(true))
            .GetResult();

        return searchResults?.ToList() ?? new List<ProductCatalogBlock>();
    }
    catch (Exception)
    {
        // Fallback or empty list
        return new List<ProductCatalogBlock>();
    }
}

The Database-Based Solution (Optimized)

As an alternative, we tested a direct IContentLoader approach using batch loading to minimize roundtrips to the SQL database:

public List<ProductCatalogBlock> GetProductsFromCategoryPage(ContentReference categoryPageRef)
{
    if (ContentReference.IsNullOrEmpty(categoryPageRef)) return new List<ProductCatalogBlock>();

    var categoryPage = _contentLoader.Get<CategoryPage>(categoryPageRef);
    
    // Batch load: Retrieve all content area items in one call
    var contentLinks = categoryPage.ProductsContentArea.Items
        .Select(i => i.ContentLink)
        .ToList();

    var allContent = _contentLoader.GetItems(contentLinks, CultureInfo.CurrentUICulture);
    
    return allContent.OfType<ProductCatalogBlock>()
                     .Where(x => x.IsActive)
                     .ToList();
}

Performance Benchmarking

We conducted benchmarks using a production-like dataset. The results challenged the initial assumption that "Find is always faster."

Approach Avg Latency 95th Percentile Complexity
Find-based ~150ms ~250ms High (Cloud IO)
Database-based ~20ms ~35ms Low (In-process)

Detailed Analysis

  • Find Approach: Scalable for massive datasets (1,000+ items) and complex filtering. However, for 25 items, the network overhead to the Find index and cloud latency becomes a bottleneck.
  • Database Approach: Extremely fast for small datasets due to Optimizely's internal level-2 caching. It provides real-time data without indexing delays.

Architectural Principles: The Right Tool

Anti-Pattern: Premature Optimization

Using Find for 25 items is a classic Premature Optimization. While the intent is to be "future-proof," it introduces unnecessary dependencies and latency for the current requirements.

Hybrid Architecture Pattern

A robust architecture adapts to the data volume. For systems where product counts vary wildly, consider a Hybrid approach:

public class ProductCatalogService : IProductCatalogService
{
    private const int FIND_THRESHOLD = 100;
    private readonly ISynchronizedObjectInstanceCache _cache;
    
    public List<ProductCatalogBlock> GetProducts(int categoryId)
    {
        var cacheKey = $"ProductCatalog_{categoryId}";
        return _cache.Get(cacheKey, () => 
        {
            int count = GetEstimateCount(categoryId);
            return count > FIND_THRESHOLD 
                ? GetFromFind(categoryId) 
                : GetFromDatabase(categoryId);
        }, TimeSpan.FromMinutes(15));
    }
}

Strategic Insights: Caching Best Practices

To maximize the Database-based approach, utilize ISynchronizedObjectInstanceCache. Unlike standard memory cache, it synchronizes across load-balanced instances.

public class OptimizedProductCatalogService
{
    private readonly ISynchronizedObjectInstanceCache _cache;
    private readonly IContentEvents _contentEvents;
    
    public OptimizedProductCatalogService(ISynchronizedObjectInstanceCache cache, IContentEvents contentEvents)
    {
        _cache = cache;
        _contentEvents.PublishedContent += OnContentPublished;
    }
    
    private void OnContentPublished(object sender, ContentEventArgs e)
    {
        if (e.Content is ProductCatalogBlock block)
        {
            _cache.Remove($"ProductCatalog_{block.CategoryId}");
        }
    }
}

Conclusion

In this case study, reverting to the database approach for small datasets was the correct architectural decision. Optimizely Find is a powerful tool, but it should be applied when the problem matches its strengths: large-scale search, complex facets, and heavy filtering.

Key Takeaway: Start simple with IContentLoader and intelligent caching. Only transition to Find when the metrics justify the added complexity and latency overhead.

#optimizely#backend#performance#cms13
← Back to Articles
GINBOK

Deep technical writing for developers and designers who care about the craft.

Content
  • All Articles
  • Engineering
  • Design
  • Product
Company
  • About Ginbok
  • Authors
  • Write for Us
  • Contact
Stay Updated
© 2026 Ginbok. All rights reserved.
PrivacyTerms