Skip to main content

Maintaining the Sitecore Helix Architecture

Like most development teams working on modern, Sitecore projects of reasonable complexity, at the agency where I work we've followed the Helix design principles and conventions for Sitecore development in structuring the solution. This means we have 3 groups of projects:

  • Foundation - at the lowest level, these projects may depend on each other but don't depend on any in the higher levels. Modules in the Foundation layer are business-logic specific extensions on the technology frameworks used in the implementation - e.g. Sitecore itself - or shared functionality between feature modules that is abstracted out.
  • Feature - these are projects in the middle layer, which may depend on Foundation projects but not on any in the higher Project level and, importantly, not on each other. Projects here should map to a business domain concept or feature.
    • We've extended the feature level in a small way by introducing the concept of sub-features. For example, for a data import feature, we have an API project, processing implementations (using Azure Function and console apps), and a common logic project. These are all classed as a single feature and dependencies within sub-features are allowed.
  • Project - this is the highest level and can depend on any of the lower levels. It's where the website output is pulled together by combining features, into a web application output. We have a common one, and then one for specific details of each of the sub-sites in our multi-site solution.

Ensuring Adherence to the Helix Architecture

There are two forms of dependencies that go against the Helix principles and rules defined above. Hard dependencies are in the form of project references - if one feature project directly depends on another one for example. In addition to developer guidance and code reviews, we have a defence against this in the form of an architectural fitness function that runs are part of the unit test suite - thus failing a build if the conventions are broken.

The other form of dependency are soft ones, that can't be detected by automated tools so need to be part of developer consideration when implementing features and checked in code reviews. These take the form of one project having "knowledge" of another one that should be off limits, perhaps via a shared name of a template or field.

If there seems to be a need to break the Helix conventions, there are a number of ways we've used to resolve it, that can be summarised into the following three methods.

Selecting the Seams

One of the challenges when selecting which projects we should have in which layers is defining the appropriate seams between different groups of functionality. The idea of a feature project in particular, is that it follows the common closure principle which states that "classes that change together are packaged together". It's quite possible that we don't get that right first time, and as the solution develops we look to refactor to either combine or split feature projects. We need to strike a balance here between having useful separation so we follow a form of single responsibility principle for a feature, but not split too much and then find we have a lot of leakage of knowledge between features.

Introducing a Foundation Project

A standard way of resolving the conflict when two feature projects need to share logic is to extract that logic into a foundation project. For example, low-level, cross-cutting concerns such as search indexing and ORM usage are defined in specific foundation projects that are referenced and shared by multiple feature ones. As we move forward, we'll likely want to introduce more of these.

Foundation projects can depend on each other, so we don't have the same concerns at this level of the architecture. However we are of course restricted in that we can't have circular references, so there still may need to be further refactoring and splitting of logic here, such that the foundation projects can depend on each other as needed.

When it comes to softer dependencies - such as knowledge of how different sites (defined at project level) or templates (defined at feature level) behave, typically here we've introduced a foundation level "register" class. These each consist of a static collection of typed objects that each higher level project can register details of at application start-up. Other projects can then reference these registers to extract the details they need for particular renderings, computed fields or other operations.

One example of this is in a class we've called AddressableTemplatesRegister - this maintains a global dictionary of registered template names for those pages that are addressable (i.e. have URLs), such that the indexing routines can ensure to index a URL for those items. The key for the dictionary is the template namd and the value is an instance of a class which has properties for related details such as canonical URL generation, page title generation, and retrieval of site map entries.

Using these each feature project can register at start-up the set of templates that it's concerned with, along with the specifics of how concerns such as those methods are defined. Individual feature projects - e.g. we have a "Navigation" one that amongst other things can enumerate the register and retrieve site map entries appropriate for each template, perhaps filtering out certain ones based on the contents of particular fields.

Delegating to Feature Projects

Whilst Sitecore is particularly extensible via it's pipeline architecture, some concepts only exist as singletons and as such can't be "decorated" by individual features. These are rare - most things, like for example item resolution, can be defined in the appropriate features, and there can be several, each handling different templates. But one example is the LinkProvider - there can only be one of these defined for the solution.

Whilst we have only one of these, defined in the common "Project" project, we delegate the specific details of link generation for different types of pages to the appropriate feature projects, making calls to methods defined on classes there.

Wrapping Up

Using a combination of these methods we've been able to maintain a solution as it's grown over 18 months to continue to be in adherence to the Helix conventions.

Comments