Core path · 23 of 34
Understand how Sling Models connect AEM repository resources to Java objects, how adaptation and injectors work internally, how OSGi services are consumed, and where the boundary between Sling Models and backend services should be drawn.
AEM stores content in the repository, but HTL is not a direct JCR API. The Java layer usually sits between the repository and the component rendering layer.
A Sling Model is that bridge. It adapts a resource or request into a Java object that exposes the specific data the component needs. That keeps the template focused on rendering, while the model handles the content-to-object mapping.
This matters when a component needs more than a simple property lookup:
If the template directly pulls repository data and business logic, the component becomes harder to test, harder to change, and harder to reason about. Sling Models give us a cleaner boundary.
The phrase "create a Sling Model and inject the properties" makes the pattern sound trivial. It is not just a Java class with annotations. The model is created by the Sling Models framework after an adaptable object is resolved.
A typical example is:
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String title;
}
The framework receives a resource, checks whether it can adapt that resource to ProductModel, and then injects the matching values into the model.
This adaptation layer is the foundation of Sling Model design.
A resource is the most common starting point for a model. It represents a content node in Sling's abstraction layer, not necessarily a raw JCR API object.
For example:
Resource resource;
A component resource often corresponds to a repository node such as:
/content/site/us/products/product-a/jcr:content/root/product
The resource can provide a ValueMap:
ValueMap properties = resource.getValueMap();
String title = properties.get("title", String.class);
This works, but it is repetitive if every component manually reads properties. Sling Models give us a cleaner way to map repository data into a Java object that HTL can render.
Adaptation is a common Sling concept. We take one object and adapt it into another form.
For example:
PageManager pageManager =
resource.getResourceResolver().adaptTo(PageManager.class);
The same idea applies to Sling Models:
ProductModel model = resource.adaptTo(ProductModel.class);
The adaptables attribute on the model tells Sling which object types can create that model. If the model is based on a resource, then resource adaptation is the natural path. If it depends on request context, a request-backed model may be more appropriate.
A basic Sling Model looks like this:
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String title;
@ValueMapValue
private String description;
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
}
The important elements are:
@Model declares the class as a Sling Model.adaptables = Resource.class defines which object can create it.@ValueMapValue reads a property from the resource's value map.The model is therefore a translation layer between repository content and the component-facing Java API.
@Model Actually Tells SlingThis is not just annotation syntax. It tells the Sling Models framework that the class is a model definition and which adaptable type creates it.
A resource-based model:
@Model(adaptables = Resource.class)
public class ProductModel {
}
means the framework can create this model from a Resource.
A request-based model:
@Model(adaptables = SlingHttpServletRequest.class)
public class ProductModel {
}
means it is created from the Sling request and can use request-scoped context.
The choice should reflect the model's responsibility. If the model is largely a wrapper around authored resource data, a resource model is usually cleaner. If it needs request parameters or rendering context, request adaptation may be the right fit.
This is one of the most common design mistakes: using the request model because it is available, even when the data is really resource data.
A resource-based model is appropriate when the model is primarily exposing authored content:
@Model(adaptables = Resource.class)
public class ProductModel {
}
A request-based model is appropriate when the component needs request-specific details such as selectors, parameters, or request attributes:
@Model(adaptables = SlingHttpServletRequest.class)
public class ProductModel {
}
The adaptable should match the model's dependency on context, not simply the most convenient object available at the call site.
@ValueMapValue WorksThe most common injection is:
@ValueMapValue
private String title;
This tells the framework to read the title property from the resource value map.
It is effectively a cleaner, declarative alternative to:
String title = resource.getValueMap().get("title", String.class);
This keeps the model focused on exposing the component data instead of repeating repository access code across the class.
By default, Sling Maps the field name to the same property name:
@ValueMapValue
private String title;
This reads title from the resource.
You can also map a different repository property name:
@ValueMapValue(name = "productTitle")
private String title;
This can be useful when the repository field name and Java model name differ, but it should not become a pattern for every property. Over-mapping adds indirection without adding value.
Not every property is required. That is why Sling Models can be configured with injection strategies.
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class ProductModel {
@ValueMapValue
private String title;
}
This is useful when a property may be absent, but it should be a deliberate choice based on the content contract.
A component with an optional image can live with a missing value. A product identifier that the application depends on should be treated as required. The model should reflect the business requirement, not just hide missing values.
This is the boundary that matters most.
A model can legitimately call an OSGi service:
@Model(adaptables = Resource.class)
public class ProductModel {
@OSGiService
private ProductService productService;
@ValueMapValue
private String sku;
public Product getProduct() {
return productService.getProduct(sku);
}
}
Here the model is doing two things well:
It should not become the place where repository access, pricing logic, external calls, retry behavior, and business rules all live. That is what the service layer is for.
@OSGiService: Connect the Model to the Runtime Service LayerThe service layer is often the correct place for reusable backend logic. Sling Models can consume it through @OSGiService:
@OSGiService
private ProductService productService;
This means the model is not creating the service. It is asking Sling to resolve a matching OSGi service from the runtime registry. The model then exposes the result to the component or the template.
That is the correct separation:
The most common injectors in AEM are:
@ValueMapValueUse for authored properties on the current resource.
@ValueMapValue
private String title;
@ChildResourceUse for child nodes or nested content structures.
@ChildResource
private ImageModel image;
@SelfUse when the model needs the current adaptable.
@Self
private Resource resource;
@ResourcePathUse when a property stores a path to another resource and the model needs the resolved resource.
@OSGiServiceUse when the model needs a runtime-managed service from the OSGi registry.
@ScriptVariable and @RequestAttributeUse when the value comes from the request or rendering context rather than the resource itself.
The pattern is simple: match the injector to the data source, not the other way around.
The repository structure should drive the Java model, not the other way around.
If the value is authored as a property on the current resource, use @ValueMapValue.
If the value exists under a child resource, use @ChildResource.
If the value is provided by the runtime context, use @ScriptVariable or @RequestAttribute.
If the logic belongs to a reusable backend capability, use @OSGiService.
This design rule avoids a common mistake: choosing annotations based on familiarity rather than the actual source of the data.
A missing value is often a debugging problem, not an injector failure.
The practical sequence is:
The annotation itself does not diagnose the problem. The source of the value does.
One of the most useful advanced patterns is model delegation. In AEM projects, it is common to extend a Core Component model instead of copying the Core Component implementation.
The pattern is straightforward:
This gives us reuse without duplicating a vendor-managed implementation.
The important idea is that delegation is a maintenance decision, not just a technical trick. It helps us avoid forking Core Component behavior while still adding project-specific data.
When designing a Sling Model, ask a few questions:
If the model is clear about those responsibilities, it is usually easier to maintain, easier to debug, and less likely to become a dumping ground for unrelated logic.
Sling Models are the Java bridge between AEM content and component rendering. They adapt repository data into a form the component can use, without forcing HTL to manage repository or service concerns directly.
The key design principle is simple:
This is what turns a Sling Model from a convenience annotation into a real architecture boundary.
The next chapter, OSGi Services in AEM — Designing the Service Layer Beyond Servlets and Sling Models, explains the service layer that owns reusable business logic instead of letting the model absorb it.
For a production decision example specific to this chapter, see When a Sling Model Earns Its Place (and When ValueMap Is Enough), which shows when a Sling Model genuinely earns its place and when raw ValueMap access is the simpler, more maintainable choice.
This is the natural boundary to understand next: Sling Models adapt content for the component, while OSGi services own the reusable capability behind it.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.