Skip to main content

Outline

At a glance:
  • Core principle: Keep CMS-specific logic separate from business logic.
  • Architectural goal: Improve maintainability, testability, and scalability.
  • Practical method: Use layered architecture, DI, and service abstractions.
  • DXP advantage: Easier upgrades and headless flexibility.

Introduction

This module explores the critical principle of Separation of Concerns (SoC) within the context of Optimizely CMS 12 and Commerce 14 development. While Optimizely provides a powerful platform for content and commerce, it is essential to distinguish between responsibilities that are inherently CMS-specific and those that represent general .NET application logic.

The Importance of Separation of Concerns

Separation of Concerns advocates dividing software into distinct sections where each section handles a specific responsibility. Applying this principle consistently in an Optimizely solution produces measurable benefits across the development lifecycle.

  • Maintainability: Changes are isolated and easier to manage - a change to CMS configuration does not ripple into business logic.
  • Testability: Components can be tested independently without requiring the CMS runtime.
  • Scalability: Independent layers can be scaled separately based on their specific load profiles.
  • Reusability: Clean, decoupled components can be reused across projects and front-end channels.
  • Easier upgrades: Platform changes have minimal impact on core logic when CMS-specific code is properly isolated.

Defining CMS-Specific Concerns

CMS-specific concerns are those that depend on Optimizely APIs, content models, or platform behaviors. They belong in the web or infrastructure layer - never in the domain layer.

CMS concern categories

Select a category to expand and read what belongs there.

Content definition and structure
  • Content types: Page, block, media, and catalog definitions.
  • Content models: Classes inheriting from PageData, BlockData, or CatalogContentBase.
  • Property editors: Custom UI editors for content fields in the CMS editing interface.
Content lifecycle
  • Loading: Using IContentLoader to retrieve content from the CMS database.
  • Saving and publishing: Using IContentRepository to create, update, and publish content programmatically.
  • Events: Handling ContentEvents to react to publish, delete, and move operations.
UI and routing
  • Rendering: Controllers and View Components tied to IContent for template-driven page rendering.
  • Edit mode enhancements: Custom CMS UI extensions for editors.
  • Optimizely routing: URL resolution logic, partial routers, and content provider integration.

Defining General .NET Responsibilities

General .NET responsibilities are those that have no inherent dependency on Optimizely. These should be expressed as platform-agnostic interfaces and implementations in the domain and application layers.

Core business logic

  • Pricing rules, fulfillment workflows, and recommendation engines.
  • Logic that is independent of how content is stored or rendered - this must never import Optimizely namespaces.

External data and integrations

  • External databases not managed by Optimizely.
  • Payment gateways, CRM, and ERP integrations.

Cross-cutting concerns

  • Logging: Serilog or Microsoft.Extensions.Logging - injected, not static.
  • Error handling: Global exception middleware for consistent error responses.
  • Security: Identity policies and authorization logic that apply independently of the CMS.
  • Configuration: General app settings managed via IConfiguration and appsettings.json.

Strategies for Achieving Separation

1. Layered architecture

  • Presentation layer: Controllers, Views, View Components - Optimizely-aware, thin.
  • Application layer: Orchestrates business workflows, coordinates domain and infrastructure.
  • Domain layer: Core business rules - no Optimizely dependency, fully testable in isolation.
  • Infrastructure layer: External services, repositories, Optimizely implementations of domain interfaces.

2. Custom service abstractions

Define business interfaces in the domain or core project with no Optimizely dependency, then implement them in the infrastructure or web project where Optimizely APIs are available. Consumers depend on the interface, not the implementation.

C#
// Core/Domain project (no Optimizely dependency) public interface IProductService { Product GetProductDetails(string sku); IEnumerable<Product> GetFeaturedProducts(); } // Optimizely implementation (Infrastructure/Web project) public class OptimizelyProductService : IProductService { private readonly IContentLoader _contentLoader; private readonly IProductCatalog _productCatalog; public OptimizelyProductService( IContentLoader contentLoader, IProductCatalog productCatalog) { _contentLoader = contentLoader; _productCatalog = productCatalog; } public Product GetProductDetails(string sku) { return _productCatalog.GetProduct(sku); } }

Note: The domain project should be able to compile and run tests without any Optimizely NuGet packages installed. If adding a reference to EPiServer.* in your domain project feels necessary, that is a signal the separation has broken down.

3. Separate projects

A practical project structure enforces separation at the build level - a project cannot accidentally import a dependency that it does not reference.

  • MySolution.Core - Domain models and interfaces; no Optimizely references.
  • MySolution.Application - Business logic and orchestration.
  • MySolution.Infrastructure - External integrations and Optimizely implementations.
  • MySolution.Web - Optimizely CMS/Commerce application; presentation layer only.

Pro tip: Use the dependency direction as a smell test: Core should never reference Infrastructure or Web. Dependencies flow inward: Web and Infrastructure depend on Application and Core, never the other way around.

Benefits in an Optimizely DXP Context

  • Easier upgrades: Less coupling to platform internals means CMS version upgrades require changes only in the infrastructure and web layers, not throughout the solution.
  • Improved testability: No CMS runtime is required for business logic tests, enabling fast, isolated unit testing.
  • Headless flexibility: The domain layer can serve multiple front-ends - traditional MVC, headless APIs, mobile apps - without modification.
  • Reduced complexity: Clear architectural boundaries improve onboarding speed and reduce the cognitive load for developers joining the project.

Conclusion

Separating CMS concerns from general .NET responsibilities is foundational for building maintainable and scalable Optimizely CMS 12 and Commerce 14 solutions. By applying layered architecture, Dependency Injection, and clear service abstractions, teams ensure that core business logic remains platform-independent.

The result is a solution that is easier to test, easier to upgrade, and capable of evolving alongside both the business and the platform without requiring wholesale rewrites.