Imagine a multi-brand enterprise rollout in AEM: you have 50 localized sites spanning five different global brands, across 30 languages. Each of these brands needs different API keys for their third-party integrations, distinct analytics tracking IDs, unique social media URLs, and varied functional toggles like "Enable Live Chat" or "Show Promotional Banner." At this scale, traditional configuration methods collapse under their own weight. If you rely purely on OSGi configurations, you end up with massive factory config proliferation that requires a deployment to change and isn't inherently tied to the content tree. If you lean on page properties, content authors are burdened with manually setting these technical values on every root page, risking human error, and your developers are forced to write complex, recursive page property traversal logic. The solution to this enterprise-scale architectural challenge is Apache Sling Context-Aware Configuration (CA-Config). It elegantly marries configuration to the content hierarchy, providing a robust, fallback-driven mechanism for resolving configuration values based on the specific content path being accessed.
In this exhaustive guide, we will cover every facet of Context-Aware Configuration in AEM. We will explore the fundamental problems it solves, the exact mechanics of the /conf fallback hierarchy, and how to define configurations using Java @Configuration interfaces. You'll see real-world, multi-tenant examples, deep dives into ConfigurationBuilder and ConfigurationResolver APIs, and how to handle nested configurations and collections. We will also dissect the CA-Config web console for debugging, its interactions with Editable Templates, and critical AEM as a Cloud Service (AEMaaCS) deployment strategies using repoinit.
For foundational knowledge, ensure you review my OSGi in AEM guide and the AEM Backend Development guide. For component-level integration, refer to the AEM Component Development guide, and for cloud-specifics, check out the AEM as a Cloud Service guide.
The problem CA-Config solves
In any non-trivial AEM implementation, you will encounter the need to configure parameters that vary based on the site, region, brand, or tenant. Consider an e-commerce integration where the North American site needs to connect to the US payment gateway endpoint, while the European sites connect to the EU endpoint.
Historically, AEM developers tried to solve this with a few anti-patterns:
-
OSGi Factory Configurations: You create an OSGi factory configuration for the payment gateway. To link it to the content, you add a
sitePathproperty (e.g.,/content/brand-a/us/en). Your Sling Model reads all factory configs, iterates through them, and finds the one that matches the current page path.- Why it fails at scale: You need a developer or an infrastructure deployment to change content-centric configurations. The matching logic is slow and custom-built. In AEMaaCS, OSGi configs are strictly read-only at runtime, requiring a full CI/CD pipeline run to change an API key.
-
Page Properties on Site Root: You add custom fields to the site's root page properties (e.g.,
/content/brand-a/us/en/jcr:content). Your components traverse up the tree usingPageManager.getContainingPage()or custom logic to find the nearest root and read the value.- Why it fails at scale: Authors can accidentally delete or modify these technical properties. The configuration logic pollutes the content tree. Complex inheritance (e.g., global -> brand -> country -> language) requires custom, brittle traversal algorithms.
-
Design Nodes / Policies: Using the legacy
/etc/designsor the newer Editable Template policies.- Why it fails at scale: Policies are meant for UI and structural component configurations, not global backend integration parameters or site-wide business logic.
Context-Aware Configuration solves these issues by standardizing how configuration is bound to the content hierarchy. It allows configurations to be stored in the /conf tree, cleanly separated from the /content tree, while mirroring its structure. It provides out-of-the-box inheritance, merging, and Java APIs for retrieving values without writing a single line of custom path-traversal code.
Configuration Mechanisms Comparison
| Feature | Context-Aware Config (CA-Config) | OSGi Configurations | Page Properties | Context-Aware OSGi Factory Configs |
|---|---|---|---|---|
| Primary Use Case | Content-specific backend/frontend settings (API keys, toggles). | System-level, low-level technical settings (DB pools, thread pools). | Content authoring metadata (SEO tags, navigation titles). | Legacy bridge for OSGi configs needing context. |
| Storage Location | /conf/<tenant>/.../sling:configs | Code repository (ui.config) runmodes. | /content/.../jcr:content | Code repository + OSGi registry. |
| Runtime Editable | Yes, by administrators/power users via UI or tools. | No, immutable in AEMaaCS. Requires pipeline run. | Yes, by any author with page edit permissions. | No, immutable in AEMaaCS. |
| Inheritance | Built-in via /conf hierarchy and sling:configRef. | None. Global per runmode. | Custom implementation required. | Minimal, relying on custom matching. |
| Type Safety | High. Defined via Java @Configuration annotations. | High. Defined via @ObjectClassDefinition. | Low. JCR properties (String, String[], etc.). | High. |
| Performance | High. Cached by Sling. | Very High. Native OSGi. | Medium to Low (if traversing up the tree). | High. |
How the /conf fallback hierarchy works
The magic of Context-Aware Configuration lies in its resolution strategy. When a component on the page /content/brand-a/us/en/home asks for a configuration, Sling doesn't just look in one place. It follows a predictable, structured fallback chain to find the most specific configuration available.
This connection between the content and the configuration is established using the sling:configRef property.
Connecting Content to Configuration
On the root page of your site (or any sub-tree where you want a configuration context to begin), you place a sling:configRef property.
/content
/brand-a
/us
/en (sling:configRef = "/conf/brand-a/us")
/homeWhen a request is made for a configuration at /content/brand-a/us/en/home, Sling examines the path. It looks up the tree for a sling:configRef. Finding it on /content/brand-a/us/en, it knows the configuration base path is /conf/brand-a/us.
The Fallback Chain
If the configuration is not found precisely at /conf/brand-a/us, Sling doesn't give up. It relies on a configured fallback path strategy (typically based on the path hierarchy). The default lookup sequence for the above example would be:
/conf/brand-a/us/sling:configs/com.example.MyConfig(Most specific)/conf/brand-a/sling:configs/com.example.MyConfig(Parent level)/conf/global/sling:configs/com.example.MyConfig(Global tenant fallback)/apps/conf/sling:configs/com.example.MyConfig(Application defaults)/libs/conf/sling:configs/com.example.MyConfig(System defaults)- Default values defined in the Java
@Configurationannotation.
This exact fallback behavior is incredibly powerful. You can define global defaults at /conf/global and only override the specific properties you need at /conf/brand-a/us.
Property Merging vs Configuration Replacement
By default, Sling CA-Config resolves the entire configuration from the most specific path that contains it. If /conf/brand-a/us has com.example.MyConfig, it uses that node. If it's missing a property, it does not look for that missing property in /conf/brand-a.
However, AEM developers often need Property Inheritance (or configuration merging). You want to define the apiUrl globally, but override the apiTimeout locally.
To enable property inheritance, you must configure the OSGi service Apache Sling Context-Aware Configuration Default Resource Inheritance Strategy (org.apache.sling.caconfig.resource.impl.def.DefaultConfigurationResourceResolvingStrategy). Checking the "Enable Configuration Inheritance" box allows Sling to merge properties from the entire fallback chain.
Defining Configurations in Java
To use CA-Config, you must first define its structure using Java annotations. This provides type safety and generates the necessary metadata.
The @Configuration Interface
Create an interface annotated with @Configuration.
package com.example.aem.core.config;
import org.apache.sling.caconfig.annotation.Configuration;
import org.apache.sling.caconfig.annotation.Property;
@Configuration(
label = "Acme API Configuration",
description = "Context-Aware Configuration for Acme API endpoints.",
name = "Acme API Config"
)
public @interface AcmeApiConfiguration {
@Property(
label = "API Endpoint URL",
description = "The root URL for the Acme API.",
order = 1
)
String apiUrl() default "https://api.acme.com/v1";
@Property(
label = "API Key",
description = "Secret key for authentication.",
order = 2
)
String apiKey() default "";
@Property(
label = "Enable Mock Data",
description = "If checked, mock data will be used instead of the live API.",
order = 3
)
boolean enableMock() default false;
@Property(
label = "Timeout (ms)",
description = "Connection timeout in milliseconds.",
order = 4
)
int timeoutMs() default 5000;
}Key Annotations:
@Configuration: Marks the interface as a CA-Config.labelanddescription: Used by UI tools (like wcm.io config editor) to render human-readable forms.@Property: Defines an individual configuration field.default: Provides the ultimate fallback value if nothing is configured in the JCR.
Nested Configurations
Configurations can be nested for better organization.
package com.example.aem.core.config;
import org.apache.sling.caconfig.annotation.Configuration;
import org.apache.sling.caconfig.annotation.Property;
@Configuration(label = "Social Media Configuration")
public @interface SocialMediaConfig {
@Property(label = "Facebook Profile URL")
String facebookUrl() default "";
@Property(label = "Twitter Profile URL")
String twitterUrl() default "";
}You can then include this within another configuration:
@Configuration(label = "Site Settings")
public @interface SiteSettingsConfig {
@Property(label = "Site Name")
String siteName();
@Property(label = "Social Settings")
SocialMediaConfig socialSettings();
}Configuration Collections
Sometimes you need a list of identical configuration structures, such as a list of regional redirects or a list of supported currencies. This is handled via Collections.
@Configuration(label = "Supported Currencies Collection")
public @interface CurrencyConfig {
@Property(label = "Currency Code (e.g., USD, EUR)")
String code();
@Property(label = "Currency Symbol")
String symbol();
}You don't embed the collection in another config interface; instead, you request it as a collection via the Java API, which we will see next.
Reading Configurations: Sling Models and APIs
Once defined, you need to read these configurations in your backend code.
Using ConfigurationBuilder in Sling Models
The most common and elegant way to access CA-Configs is within a Sling Model using the @ContextAwareConfiguration annotation or by adapting the resource to ConfigurationBuilder.
Method 1: Direct Annotation (Recommended for simplicity)
The @ContextAwareConfiguration injector handles the lookup automatically based on the current resource.
package com.example.aem.core.models;
import com.example.aem.core.config.AcmeApiConfiguration;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ContextAwareConfiguration;
import javax.annotation.PostConstruct;
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class ApiClientModel {
// This automatically injects the resolved configuration!
@ContextAwareConfiguration
private AcmeApiConfiguration apiConfig;
private String endpoint;
private boolean isMocked;
@PostConstruct
protected void init() {
if (apiConfig != null) {
this.endpoint = apiConfig.apiUrl();
this.isMocked = apiConfig.enableMock();
}
}
public String getEndpoint() { return endpoint; }
public boolean isMocked() { return isMocked; }
}Method 2: Using ConfigurationBuilder Adaption
If you need more control, such as reading a collection or handling dynamic config names, adapt the resource to ConfigurationBuilder.
package com.example.aem.core.models;
import com.example.aem.core.config.AcmeApiConfiguration;
import com.example.aem.core.config.CurrencyConfig;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.ConfigurationBuilder;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class MultiConfigModel {
@Self
private Resource currentResource;
private String apiUrl;
private List<String> supportedCurrencies;
@PostConstruct
protected void init() {
if (currentResource != null) {
ConfigurationBuilder builder = currentResource.adaptTo(ConfigurationBuilder.class);
if (builder != null) {
// Read a single config
AcmeApiConfiguration apiConfig = builder.as(AcmeApiConfiguration.class);
this.apiUrl = apiConfig.apiUrl();
// Read a collection of configs
Collection<CurrencyConfig> currencies = builder.asCollection(CurrencyConfig.class);
this.supportedCurrencies = currencies.stream()
.map(CurrencyConfig::code)
.collect(Collectors.toList());
}
}
}
}The ConfigurationResolver Service API
In OSGi services, you don't always have an adaptable Resource context right away, or you might prefer standard OSGi service injection. Use the ConfigurationResolver service.
package com.example.aem.core.services.impl;
import com.example.aem.core.config.AcmeApiConfiguration;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.ConfigurationBuilder;
import org.apache.sling.caconfig.ConfigurationResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(service = ApiCallerService.class)
public class ApiCallerServiceImpl implements ApiCallerService {
private static final Logger log = LoggerFactory.getLogger(ApiCallerServiceImpl.class);
@Reference
private ConfigurationResolver configurationResolver;
public void executeApiCall(Resource contentResource) {
// Resolve configuration for the specific content path
ConfigurationBuilder builder = configurationResolver.get(contentResource);
AcmeApiConfiguration config = builder.as(AcmeApiConfiguration.class);
log.info("Making call to URL: {} with Timeout: {}", config.apiUrl(), config.timeoutMs());
// Execute HTTP request using config values...
}
}A Complete Multi-Tenant Example
Let's look at the JCR structure for a complex multi-tenant scenario. We have Brand A, operating in the US, UK, and DE.
Content Structure
/content
/brand-a (sling:configRef = "/conf/brand-a")
/us (sling:configRef = "/conf/brand-a/us")
/en
/uk (sling:configRef = "/conf/brand-a/uk")
/en
/de (sling:configRef = "/conf/brand-a/de")
/deConfiguration Structure (/conf)
We define the default API URL globally, but the German site requires a distinct localized API endpoint. The UK site requires a different API Key.
/conf
/global
/sling:configs
/com.example.aem.core.config.AcmeApiConfiguration
- apiUrl = "https://api.global.acme.com"
- enableMock = false
/brand-a
/sling:configs
/com.example.aem.core.config.AcmeApiConfiguration
- apiKey = "BRAND_A_GLOBAL_KEY"
/us
// Uses fallback. Will resolve:
// apiUrl = "https://api.global.acme.com" (from global)
// apiKey = "BRAND_A_GLOBAL_KEY" (from brand-a)
/uk
/sling:configs
/com.example.aem.core.config.AcmeApiConfiguration
- apiKey = "BRAND_A_UK_SPECIFIC_KEY"
// Will resolve:
// apiUrl = "https://api.global.acme.com" (from global)
// apiKey = "BRAND_A_UK_SPECIFIC_KEY" (overridden here)
/de
/sling:configs
/com.example.aem.core.config.AcmeApiConfiguration
- apiUrl = "https://api.acme.de/v1"
// Will resolve:
// apiUrl = "https://api.acme.de/v1" (overridden here)
// apiKey = "BRAND_A_GLOBAL_KEY" (from brand-a)Note: For this property merging to work exactly as described above, you must have the OSGi configuration org.apache.sling.caconfig.resource.impl.def.DefaultConfigurationResourceResolvingStrategy set with configCollectionInheritance and configPropertyInheritance enabled.
CA-Config and the Web Console: Debugging Resolution
Debugging CA-Config resolution issues can be a headache without the right tools. Thankfully, Sling provides an excellent web console plugin.
Navigate to: http://localhost:4502/system/console/slingcaconfig
This console is your best friend. It allows you to:
- Test Configuration Resolution: Enter a content path (e.g.,
/content/brand-a/us/en) and the fully qualified class name of your configuration interface. - View Fallback Paths: It will print exactly which paths it searched to find the configuration.
- View Resolved Properties: It outputs the final merged properties that the code will receive, showing exactly which
/confpath contributed which property.
Common Debugging Scenarios:
- "My configuration is always returning default values!"
- Check: Ensure the
sling:configRefproperty exists on the content path or its ancestors. - Check: Ensure the JCR nodes in
/confare strictly named after the fully qualified class name (e.g.,com.example.aem.core.config.AcmeApiConfiguration), unless you explicitly defined a@Configuration(name="customName").
- Check: Ensure the
- "Property merging isn't working!"
- Check: Verify the DefaultConfigurationResourceResolvingStrategy OSGi config has inheritance enabled.
CA-Config and Editable Templates
There is often confusion regarding how CA-Config relates to Editable Templates and Policies.
- Policies reside in
/conf/<tenant>/settings/wcm/policiesand dictate the authoring behavior and design of a component (e.g., "allowed styles", "allowed components in a parsys"). - Context-Aware Configurations reside in
/conf/<tenant>/sling:configsand dictate the business logic and backend integration parameters for the site.
When you create a site utilizing Editable Templates, AEM automatically sets the cq:conf property on the site root to point to the /conf folder for template resolution.
Important Distinction: cq:conf is strictly used by AEM WCM to find templates and policies. sling:configRef is used by Apache Sling to find Context-Aware Configurations. While they often point to the exact same /conf/<tenant> path, they are separate mechanisms. In modern AEM projects, AEM will fallback to using cq:conf as the configuration reference if sling:configRef is not present, thanks to an out-of-the-box compatibility provider. However, explicitly setting sling:configRef is considered a cleaner architectural practice, especially for deep multi-brand hierarchies where template configuration and API configuration might diverge at lower levels.
AEMaaCS Considerations: Immutable /conf and Repoinit
AEM as a Cloud Service enforces strict read-only states for code and configuration directories (/apps and /libs). The /conf directory is a hybrid.
/conf/<tenant>/settings/wcm/templates: Mutable (Authors can create templates in production)./conf/<tenant>/sling:configs: Mutable at runtime via JCR API, but typically initialized via code.
Because /conf is part of the mutable content area in AEMaaCS, deploying configurations via standard XML package deployment (like you would for /apps) can overwrite changes made in the production environment if you aren't careful with filter.xml rules (using mode="merge").
The Standard Approach: UI Editors
The industry standard for managing CA-Configs is to use a UI tool. AEM provides basic support, but the wcm.io Context-Aware Configuration Editor is the undisputed champion. It auto-generates beautiful AEM Touch UI forms based on your Java @Configuration annotations, allowing administrators to edit API keys and toggles directly in production without a code deployment.
The Bootstrapping Approach: Repoinit If you need to guarantee certain default configurations exist in a fresh AEMaaCS environment, do not package them as XML. Use repoinit.
In your ui.config project's org.apache.sling.jcr.repoinit.RepositoryInitializer-custom.config:
scripts=[
"
create path /conf/global/sling:configs(sling:Folder)
set properties on /conf/global/sling:configs/com.example.aem.core.config.AcmeApiConfiguration
set apiUrl{String} to \"https://api.production.acme.com\"
set enableMock{Boolean} to false
end
"
]Repoinit executes safely on startup and ensures the nodes exist without wiping out subsequent runtime modifications.
Cheat Sheet
- Annotation:
@org.apache.sling.caconfig.annotation.Configuration - Property Annotation:
@org.apache.sling.caconfig.annotation.Property - Node Location:
/conf/<context>/sling:configs/<fully.qualified.ClassName> - Connection Property:
sling:configRef(String property on/contentnodes). - Injection (Models):
@ContextAwareConfiguration - Manual Resolution:
resource.adaptTo(ConfigurationBuilder.class) - Service Resolution:
@Reference ConfigurationResolver - Debugging Console:
/system/console/slingcaconfig
Best Practices
- Always specify default values: In your
@Configurationinterface, always provide adefaultvalue for strings, booleans, and numbers. This preventsNullPointerExceptionsif the configuration hasn't been authored in the JCR yet. - Use wcm.io CA Config Editor: Do not rely on crx/de to manage these configurations. Install the wcm.io extension. It reads your annotations and provides a native-looking AEM interface for authors/admins to manage these values securely.
- Group logically: Don't create one massive "SiteConfig" with 50 properties. Create specific configurations:
AnalyticsConfig,PaymentGatewayConfig,SearchConfig. - Leverage Global Fallback: Put your production defaults in
/conf/globaland only create nodes in/conf/brand-awhen overriding is strictly necessary. This dramatically reduces configuration duplication. - Secure your configs: If storing sensitive API keys, ensure the ACLs on the
/conf/<tenant>/sling:configstree restrict read access to only the necessary service users. Authors shouldn't be able to read production secrets.
Do's & Don'ts
- DO use CA-Config for any backend property that varies by site, region, or brand.
- DON'T use OSGi configurations for tenant-specific settings. Reserve OSGi configs for low-level system settings (thread pools, dispatcher flush agent URLs, OSGi component tuning).
- DO use collections for lists of similar items (e.g., country dropdown data, redirect maps).
- DON'T rely on page properties for integration parameters. Keep the content tree clean.
- DO test your fallback hierarchy thoroughly using the Web Console plugin before deploying.
- DON'T deploy
/conf/.../sling:configsviaui.appswithmode="replace". You will wipe out production configurations on every deployment. Use repoinit ormode="merge"inui.content.
Mastering Context-Aware Configuration is a hallmark of a Senior AEM Developer. It ensures your architecture remains resilient, scalable, and beautifully separated from content authoring concerns, regardless of how many global sites your enterprise launches.
Discussion
Loading discussion…
Try a related tool
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.