Sling Models in AEM — From Repository Data to Component Logic
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.
The Problem Between AEM Content and HTL
AEM stores content in the repository.
For a simple component, the repository might contain:
/content/site/us/products/product-a
├── jcr:primaryType = cq:Page
└── jcr:content
├── sling:resourceType = myproject/components/product
├── title = "Product A"
├── sku = "ABC-123"
└── description = "Product description"
HTL, however, doesn't work directly with JCR nodes in the way a developer might initially expect.
Suppose the component needs:
- title
- description
- product SKU
- calculated price
- availability
We don't want the HTL template to contain repository access and business logic.
Instead, we want a Java layer to prepare the data required by the component:
HTL
↓
ProductModel
↓
Repository / OSGi Service
This is where Sling Models fit.
A Sling Model can adapt the AEM resource and expose the data that the component needs, while reusable business logic can remain in an OSGi service.
Before Sling Models: Where the Confusion Starts
AEM developers often hear:
- "Create a Sling Model and inject the properties."
That makes it sound like the model is simply a Java object with some annotations.
There is more happening underneath.
A Sling Model is created by the Sling Models framework when an adaptable object can be converted into the requested model class.
For example:
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String title;
}

The model doesn't directly open the repository and search for a node.
Instead, the framework starts with an adaptable object such as a Resource and tries to create the model from it.
That distinction is important because adaptation is the foundation of Sling Models.
The Resource Is the Starting Point
Let's start with something AEM developers already encounter:
Resource resource;
A Resource represents a resource in Sling's resource abstraction.
For a component, it commonly represents the component's repository resource.
For example:
/content/site/us/products/product-a/jcr:content/root/product
The resource can provide access to properties:
ValueMap properties = resource.getValueMap();
String title = properties.get("title", String.class);
This works.
But if every component reads properties this way, the Java code can become repetitive:
String title;
String description;
String sku;
String image;
String category;
Sling Models give us a cleaner way to map that resource data into a Java model.
Adaptation Is the Key Concept
AEM and Sling use an adaptation mechanism in many places.
The basic idea is:
One object
↓
adaptTo(...)
↓
Another representation
For example:
PageManager pageManager =
resource.getResourceResolver().adaptTo(PageManager.class);
The same idea is used with Sling Models:
ProductModel model =
resource.adaptTo(ProductModel.class);
The Sling Models framework checks whether the requested model can be created from the adaptable object.
So:
Resource
↓
adaptTo(ProductModel.class)
↓
ProductModel
This is why the adaptables declaration on a Sling Model matters.
Defining a Sling Model
A basic Sling Model might look 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 pieces are:
@Model
↓
Defines the class as a Sling Model
adaptables = Resource.class
↓
The model can be created from a Resource
@ValueMapValue
↓
Injects a property from the resource's ValueMap
The model is therefore acting as a bridge:
AEM Repository
↓
Resource
↓
Sling Model
↓
HTL
What Does @Model Actually Tell Sling?
Consider:
@Model(adaptables = Resource.class)
public class ProductModel {
}
The @Model annotation tells the Sling Models framework that this class is a model definition.
The adaptables attribute tells the framework which object types can be used to create the model.
For example:
@Model(adaptables = Resource.class)
means the model can be created from a Resource:
Resource
↓
Sling Models Framework
↓
ProductModel
You can also encounter models adaptable from a Sling HTTP request:
@Model(adaptables = SlingHttpServletRequest.class)
In that case, the model is created from the request:
SlingHttpServletRequest
↓
Sling Models Framework
↓
ProductModel
The choice depends on what the model actually needs.
A model that only represents repository content is often naturally resource-based.
@Model(adaptables = Resource.class)
A model that needs request-specific information may use:
@Model(adaptables = SlingHttpServletRequest.class)
This is not just a syntax choice.
The adaptable determines what object the Sling Models framework starts with when creating the model and therefore what context is available to the model.
Resource Adaptable vs Request Adaptable
Consider two models.
Resource-based model
@Model(adaptables = Resource.class)
public class ProductModel {
}
The model starts with:
Resource
It can work with repository content and resource-related context.
Request-based model
@Model(adaptables = SlingHttpServletRequest.class)
public class ProductModel {
}
The model starts with:
SlingHttpServletRequest
Now request-specific information can be relevant.
For example:
Request
├── Resource
├── Selectors
├── Extensions
├── Parameters
└── Request attributes
The choice should follow the model's responsibility.
Don't use a request adaptable simply because it is available.
If the model only needs the resource, keeping it resource-based makes the dependency clearer.
How @ValueMapValue Works
Now let's look at the most common injection:
@ValueMapValue
private String title;
Suppose the component resource contains:
title = "Product A"
The model framework can obtain the resource's ValueMap and resolve the title property:
Resource
│
▼
ValueMap
│
└── title
│
▼
@ValueMapValue
│
▼
ProductModel.title
Without Sling Model injection, we could read the property manually:
String title =
resource.getValueMap().get("title", String.class);
With @ValueMapValue, the property can be injected directly into the model:
@ValueMapValue
private String title;
This keeps the model focused on exposing the data needed by the component instead of repeatedly accessing the ValueMap.
Property Name and Java Field Name
By default, Sling Models can use the field name as the property name.
For example:
@ValueMapValue
private String title;
maps to:
Repository property
title
↓
Java field
title
But the Java name doesn't always have to match the repository property.
You can explicitly specify the property:
@ValueMapValue(name = "productTitle")
private String title;
Now the relationship is:
Repository property
productTitle
↓
Java field
title
This can be useful when the Java model uses a cleaner domain name than the underlying repository property.
@ValueMapValue(name = "productTitle")
private String name;
The model can expose name while the repository continues to use productTitle.
However, don't introduce unnecessary mappings.
Repository property names are part of the content structure, and excessive translation between repository names and Java field names can make the model harder to understand.
Required vs Optional Injection
Another decision developers encounter is what should happen when a property is missing.
Suppose:
@ValueMapValue
private String title;
but the repository doesn't contain title.
The behavior depends on the injection strategy and how the model and injector are configured.
The important thing is to be deliberate about which properties are actually required.
For example, if a title is optional:
title
↓
May be missing
↓
Model still usable
If a property is essential to the model:
productId
↓
Required
↓
Missing value should be treated as a content/model problem
The model can explicitly define its injection strategy when required:
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class ProductModel {
@ValueMapValue
private String title;
}
Or, for a property that must be available, the model can use the required injection behavior rather than making everything optional.
The key is not to make every field optional just to prevent injection problems.
Optional content
↓
Optional injection
Required content
↓
Required injection
The injection strategy should reflect the component's actual content contract.
This matters in production because incorrectly authored content should be distinguishable from a problem in the Java implementation.
Why Injection Strategy Matters
Consider an image component.
The image path might be optional:
@ValueMapValue
private String imagePath;
If an author hasn't selected an image, the model can still exist.
The getter can handle the missing value:
public boolean hasImage() {
return imagePath != null && !imagePath.isEmpty();
}
That is different from a component where a value is fundamental to its operation.
For example:
Product Component
│
└── productId
↓
Required
If productId is missing, the component may not be able to perform its intended operation.
The model should therefore reflect the component's actual content contract rather than blindly making every property optional.
Optional content
↓
Handle missing value
Required content
↓
Treat missing value as a content/model problem
This keeps the injection strategy aligned with how the component is actually expected to work.
The Model Should Not Become a Business Logic Layer
This is where our Chapter 22 discussion about OSGi services connects directly.
Suppose we have:
@Model(adaptables = Resource.class)
public class ProductModel {
@OSGiService
private ProductService productService;
@ValueMapValue
private String sku;
public Product getProduct() {
return productService.getProduct(sku);
}
}
This is a reasonable separation.
The model:
Reads component data
↓
Gets SKU
↓
Calls ProductService
↓
Exposes result to HTL
The service:
ProductService
↓
Business logic
↓
Repository / API / Pricing / Other Services
The model shouldn't become:
ProductModel
├── Repository queries
├── HTTP calls
├── Pricing rules
├── Authentication
├── Retry logic
└── Business decisions
That would make the Sling Model responsible for much more than adapting component data.
The boundary we want is:
Sling Model
↓
Presentation / Component Data
↓
OSGi Service
↓
Reusable Business Logic
This is exactly the boundary we established in the OSGi Services chapter.
@OSGiService Connects Sling Models to OSGi
This is one of the most useful connections between the two chapters.
In the OSGi Services chapter, we saw how an OSGi service is registered and made available through the OSGi service registry.
Now the Sling Model becomes a consumer of that service.
For example:
@OSGiService
private ProductService productService;
The Sling Models framework uses the OSGi service injector to obtain the service.
This is different from using @Reference directly on a normal Sling Model.
That's an important distinction:
Both ultimately connect application code to an OSGi service, but they operate through different component models.
A Complete Component Flow
Let's put the pieces together.
Suppose an author opens a product page.
The repository contains:
/content/site/us/products/product-a
│
└── jcr:content
│
└── product
├── sku = "ABC-123"
└── title = "Product A"
The component has:
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String sku;
@ValueMapValue
private String title;
@OSGiService
private ProductService productService;
public Product getProduct() {
return productService.getProduct(sku);
}
}
This is the basic bridge between the AEM content layer and the backend service layer.
The model adapts repository content and prepares data for the component, while the OSGi service handles reusable backend logic.
Where We Are Going Next
We've seen how a model can read properties directly from the resource and also use an OSGi service.
The next question is:
How does Sling decide where an injected value should come from?
That's where Sling Model Injectors become important.
Sling Model Injectors: Where Does the Value Actually Come From?
When we write:
@ValueMapValue
private String title;
it is easy to think:
- "Sling automatically knows where title comes from."
It doesn't work quite that simply.
The Sling Models framework has different injectors that know how to obtain different kinds of values.

Each injector has a different source.
That gives us a better way to understand Sling Models:
- The annotation tells the framework what kind of object or value we are asking it to obtain.
@ValueMapValue: Read a Property
This is probably the most common injection in component models.
Suppose the component resource contains:
title = "AEM Architecture"
description = "Understanding Sling Models"
The model can map those properties:
@ValueMapValue
private String title;
@ValueMapValue
private String description;
The source is the resource's ValueMap.
That's the important part.
When you see:
@ValueMapValue
think:
- "Read a property from the resource."
@ChildResource: Move to a Child Resource
Now consider a component that contains a child resource.
For example:
product
├── title = "Product A"
├── sku = "ABC-123"
│
└── image
├── fileReference = "/content/dam/products/a.jpg"
└── alt = "Product A"
The image isn't simply another property of the product resource.
It is a child resource.
The model can represent that relationship:
@ChildResource
private ImageModel image;
This is different from:
@ValueMapValue
private String fileReference;
because the data is stored differently.
That distinction becomes very useful when looking at the repository structure.
Repository Structure Should Influence the Model
Suppose we have:
product
├── title
├── sku
└── image
├── fileReference
└── alt
A natural model structure is:
ProductModel
│
├── title
├── sku
└── ImageModel
├── fileReference
└── alt
The Java structure follows the repository structure.
This is one of the advantages of using Sling Models.
Instead of having one large class with dozens of unrelated properties, the model can represent the structure of the component.
@Self: Use the Current Adaptable
Another injector you'll encounter is:
@Self
private Resource resource;
If the model is adaptable from a Resource, @Self can provide the current adaptable.
Conceptually:
Current Adaptable
│
▼
@Self
│
▼
Resource
For example:
@Model(adaptables = Resource.class)
public class ProductModel {
@Self
private Resource resource;
public String getPath() {
return resource.getPath();
}
}
The model isn't asking the framework to search the repository for another resource.
It is asking for the adaptable that was used to create the model.
That's why @Self is useful when the model needs access to its current context.
@ResourcePath: Resolve Another Resource
Sometimes a component contains a path to another resource.
For example:
relatedProductPath =
/content/site/us/products/product-b
A model may need to resolve that path into a Resource.
This is where @ResourcePath can be useful.
For example:
@ResourcePath
private Resource relatedProduct;
The exact usage depends on how the path is stored and how the injector is configured.
The important architectural idea is:
Stored path
↓
Resource
This is different from @ValueMapValue, which simply reads the path string.
String Path vs Resource
Consider this property:
relatedProduct =
/content/site/us/products/product-b
With:
@ValueMapValue
private String relatedProduct;
you get the path itself:
"/content/site/us/products/product-b"
With an appropriate resource-path injection, the model can work with the actual resource:
Resource
path = /content/site/us/products/product-b
That difference matters when the model needs to read properties from the referenced resource.
It also means the model is moving from:
String
to:
Repository Resource
which is a different responsibility.
@OSGiService: Obtain a Backend Service
We've already seen this one in the previous section:
@OSGiService
private ProductService productService;
The source isn't the repository.
The source is the OSGi service registry.
This is why the previous OSGi chapter matters here.
The Sling Model doesn't create the service.
It asks the Sling Models framework to obtain the matching OSGi service.
So when you see:
@OSGiService
think:
- "Get this dependency from OSGi."
@ScriptVariable: Values From the Sling/AEM Request Context
Some values aren't stored directly on the component resource.
AEM and Sling expose contextual objects through the request and script context.
For example, a component may need access to the current page.
Depending on the model's adaptable and context, values such as AEM's currentPage can be injected using @ScriptVariable.
For example:
@ScriptVariable
private Page currentPage;
The important concept is that this value comes from the request/script context rather than the component resource.
This isn't:
Component Resource
↓
Property
It's contextual information associated with rendering.
That distinction is important when deciding whether something belongs in the repository model or comes from the current request/rendering context.
@RequestAttribute: Data Attached to the Request
A request can also contain attributes added by other parts of the application.
For example:
@RequestAttribute
private String requestMode;
The value isn't necessarily stored in the repository.
This can be useful when another part of the request processing pipeline has deliberately placed information into the request.
Again, the source matters.
If the value is content authored by an author, it generally belongs in the repository.
If the value exists only for the current request, request context may be the appropriate source.
One Component Can Use Multiple Injectors
A real model can use more than one injector.
For example:
@Model(adaptables = Resource.class)
public class ProductModel {
@ValueMapValue
private String title;
@ValueMapValue
private String sku;
@ChildResource
private ImageModel image;
@OSGiService
private ProductService productService;
@Self
private Resource resource;
}
The model is now pulling information from different places:
ProductModel
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
ValueMap Child Resource OSGi Service
│ │ │
title/sku image ProductService
│ │ │
└───────────────────┴───────────────────┘
│
▼
Model Output
This is why knowing the injector source is more useful than simply knowing the annotation name.
Choosing the Injector From the Repository Structure
Suppose you inspect CRXDE and find:
product
│
├── title
├── sku
│
├── image
│ ├── fileReference
│ └── alt
│
└── relatedProduct
A reasonable model might be:
@ValueMapValue
private String title;
@ValueMapValue
private String sku;
@ChildResource
private ImageModel image;
@ValueMapValue
private String relatedProduct;
The mapping follows the storage structure.
If relatedProduct is later resolved into another resource, the model can use an appropriate resource-path approach.
The key is to understand the repository first.
Don't start by choosing annotations.
Start by asking:
- Where is the data actually stored?
Then choose the injector that matches that source.
A Useful Injector Decision Tree
When adding a field to a Sling Model, this simple decision process can help:
Where does the value come from?
│
┌──────┼─────────┬─────────────┐
│ │ │ │
▼ ▼ ▼ ▼
Property Child OSGi Service Request /
Resource Context
│ │ │ │
▼ ▼ ▼ ▼
ValueMap Child OSGiService Appropriate
Value Resource Context Injector
For example:
title
↓
Property
↓
@ValueMapValue
image
↓
Child resource
↓
@ChildResource
ProductService
↓
OSGi service
↓
@OSGiService
currentPage
↓
AEM/Sling context
↓
@ScriptVariable
This way of thinking prevents the common habit of choosing annotations based only on what we've seen in another component.
The Injector Is Not the Business Logic
One more boundary is worth keeping clear.
Suppose we have:
@ValueMapValue
private String sku;
The injector's job is to obtain the SKU.
It shouldn't also:
Call commerce API
Calculate price
Validate customer
Apply discount
Check inventory
That belongs elsewhere.
The model can do:
Repository
↓
SKU
↓
ProductService
↓
Product Data
So the responsibilities remain:
Injector
↓
Obtain value
Sling Model
↓
Adapt / prepare component data
OSGi Service
↓
Reusable business logic
This separation becomes increasingly important as components become more complex.
What Happens When Injection Fails?
This is where the injector mental model becomes useful for debugging.
Suppose:
@ValueMapValue
private String title;
returns null.
Don't immediately assume the injector is broken.
Check the repository first.
Component Resource
│
▼
Does property "title" exist?
│
┌──┴──┐
│ │
Yes No
│ │
▼ ▼
Check Content
model issue
If the property exists, check whether:
- The field name matches the property name
- An explicit property name was configured
- The adaptable provides the required context
- The injection strategy allows the value to be absent
- The model is actually being created from the resource you expect
For a child resource:
@ChildResource
↓
Does the child node/resource exist?
For an OSGi service:
@OSGiService
↓
Is the service registered?
Is the service active?
The annotation alone doesn't tell you where the failure is.
The source of the injection does.
A Production Debugging Example
Suppose a product component suddenly stops showing its title.
The model contains:
@ValueMapValue
private String title;
The debugging path should be straightforward:
HTL
↓
ProductModel
↓
title = null
↓
What resource created the model?
↓
Inspect that resource
↓
Does "title" exist?
If the repository contains:
title = "Product A"
continue:
Property exists
↓
Is model adapted from expected resource?
↓
Is the property name correct?
↓
Is injection configured correctly?
This is much faster than changing annotations randomly.
The Most Important Sling Model Mental Model
By now, the relationship should look like this:
Sling Model
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Repository AEM/Sling OSGi Runtime
Data Context │
│ │ │
▼ ▼ ▼
@ValueMapValue @ScriptVariable @OSGiService
@ChildResource @RequestAttribute
@ResourcePath
│ │ │
└────────────────┼─────────────────┘
▼
Model Output
│
▼
HTL
The model is a composition point.
It gathers the information required by the component and exposes it in a form that the presentation layer can use.
It should not become the place where every backend responsibility is implemented.
That distinction will become even more important when we look at model delegation, inheritance, and how Core Components use Sling Models.
Sling Models and Core Components: Where the Architecture Gets Interesting
So far, we've looked at Sling Models mostly from the perspective of our own components.
But in a real AEM project, we don't build everything from scratch.
A lot of the time, we start with an AEM Core Component and extend its behavior.
This is where Sling Models become more interesting.
Suppose the project uses the Core Component Image.
The component already knows how to handle things such as:
Image
├── Asset
├── Image URL
├── Alt Text
├── Width
├── Height
└── Responsive behavior
The project may only need to add one business-specific requirement.
For example:
Core Image Model
│
├── Existing image behavior
│
└── Project-specific metadata
Creating a completely new image model and duplicating the existing behavior would create unnecessary maintenance.
A better approach can be to reuse the existing model and add only what the project needs.
This is where Sling Model delegation becomes useful.

Why Delegation Exists
Imagine a Core Component already provides:
public interface Image {
String getSrc();
String getAlt();
String getTitle();
}
Our project needs:
Existing Core Component behavior
+
Product-specific tracking information
We don't want to rewrite:
getSrc()
getAlt()
getTitle()
just to add:
getTrackingId()
The architecture we want is:
Core Component Model
│
│ existing behavior
▼
Project Model
│
└── additional project behavior
The project model delegates the existing functionality to the Core Component model.
That gives us reuse without copying the implementation.
The Delegation Pattern
A simplified example looks like this:
@Model(
adaptables = Resource.class,
adapters = Image.class,
resourceType = "myproject/components/image"
)
public class ImageModel implements Image {
@Self
@Via(type = ResourceSuperType.class)
private Image delegate;
public String getSrc() {
return delegate.getSrc();
}
public String getAlt() {
return delegate.getAlt();
}
public String getTitle() {
return delegate.getTitle();
}
public String getTrackingId() {
return "product-image";
}
}
The important part is:
@Self
@Via(type = ResourceSuperType.class)
private Image delegate;
The model is asking Sling to obtain the implementation associated with the resource's resource super type.
Our model can then reuse the Core Component behavior.
Why Resource Super Type Matters
This connects directly to Sling Resource Resolution.
Suppose our component is:
/apps/myproject/components/image
and its resource super type points to a Core Component:
/apps/myproject/components/image
│
│ sling:resourceSuperType
▼
/libs/core/wcm/components/image/v3/image
Now Sling has an inheritance relationship:
My Project Component
│
└── inherits from
↓
Core Image Component
This doesn't mean the project component physically copies the Core Component.
It means Sling can fall back to the super type when resolving resources and behavior.
That is why the delegation pattern works so well with Core Components.
What the Delegation Actually Gives Us
Suppose the Core Component already implements:
getSrc() getAlt() getTitle() getWidth() getHeight()
Our project model doesn't need to reproduce all of that.
Instead:
Project Model
│
┌───────────┴───────────┐
│ │
▼ ▼
Delegated methods Custom methods
│ │
▼ ▼
Core Component Project Logic
For example:
public String getSrc() {
return delegate.getSrc();
}
public String getAlt() {
return delegate.getAlt();
}
public String getTrackingId() {
return "product-image";
}
The existing behavior remains with the Core Component.
The project model only owns the additional behavior.
This Is Different From Copying a Core Component
Consider two approaches.
Approach 1: Copy the implementation
Core Image
↓
Copy Java code
↓
Project Image
↓
Modify code
Now the project owns a copy of the Core Component implementation.
When Adobe changes or improves the Core Component, our copy doesn't automatically receive those improvements.
We have to maintain the fork ourselves.
Approach 2: Delegate
Core Image
↑
│
Project Image Model
│
└── custom behavior
The project reuses the existing behavior and only adds what is necessary.
This generally gives us a cleaner maintenance boundary.
The architectural principle is:
- Extend behavior where necessary instead of copying behavior that already works.
Delegation Doesn't Mean "Customize Everything"
There is another important point.
Just because delegation is available doesn't mean every Core Component should receive a custom Sling Model.
If the Core Component already does everything the project needs:
Core Component
↓
Use it directly
No custom model is necessary.
If the project needs a small addition:
Core Component
+
Small project requirement
↓
Delegation may be appropriate
If the project needs behavior that is fundamentally different:
Core Component
↓
Majorly different requirements
↓
Reconsider the component architecture
The goal is not to customize Core Components as much as possible.
The goal is to avoid unnecessary duplication while still meeting the application's requirements.
A Real Project Scenario
Imagine an enterprise website has hundreds of pages.
The project standardizes on the AEM Core Image Component.
Marketing then asks for an additional requirement:
- Every image should expose a tracking identifier to the analytics layer.
The Core Component doesn't provide the project-specific identifier.
We could modify every component implementation.
Or we could extend the model:
Core Image Model
│
├── Image rendering
├── Asset handling
├── Alt text
└── Responsive behavior
│
▼
Project Model
│
└── Analytics tracking ID
Now the responsibility is clear.
Adobe's component owns image behavior.
The project owns analytics-specific behavior.
Delegation and HTL
The benefit becomes visible when HTL consumes the model.
For example:
<img
src="${image.src}"
alt="${image.alt}"
data-tracking-id="${image.trackingId}"
/>
HTL doesn't need to know whether:
src
came from:
Core Component
or whether:
trackingId
came from:
Project-specific logic
The model presents one clean interface.
This is exactly the kind of separation we want at the component boundary.
Delegation vs Inheritance
These concepts are related but shouldn't be treated as the same thing.
At the AEM component level, we may have:
sling:resourceSuperType
which establishes component inheritance through Sling resource resolution.
At the Java level, our model may use delegation:
Project Model
↓
Core Model
There are two different relationships here. sling:resourceSuperType controls component inheritance at the Sling resource level. The Java model delegation is a separate mechanism used to reuse the Core Component model.
Understanding both layers is important.
Otherwise, it is easy to assume that changing the resource super type automatically changes the Java model in exactly the same way.
The runtime pieces work together, but they are not the same mechanism.
A Common Mistake: Putting Everything in the Model
Delegation solves one problem, but another problem can appear.
A developer starts with:
Core Model
↓
Project Model
Then gradually adds:
Project Model
├── Repository queries
├── External API calls
├── Pricing logic
├── Analytics logic
├── Formatting
├── Authentication
└── Business rules
The model eventually becomes the application's backend layer.
That's not what we want.
The model should primarily prepare the data required by the presentation layer.
A healthier structure is:
HTL
↓
Sling Model
├── Component data
├── Presentation decisions
└── Calls services
↓
OSGi Services
↓
Business Logic
This is the same boundary we established earlier.
Where Should Logic Go?
A useful question is:
- "Does this logic exist because the component needs to render something, or because the application has a business rule?"
For example:
public String getDisplayTitle() {
return title != null ? title.trim() : "";
}
This can reasonably live in the model if it is purely presentation-related.
But:
public Price calculateCustomerPrice() {
// customer eligibility
// pricing rules
// promotions
// external pricing service
}
belongs behind a service boundary.
The distinction keeps the model from becoming a "god class."
What Happens If the Delegated Model Is Not Available?
This is another useful production consideration.
This is why debugging the model should sometimes start with the repository resource type.
When delegation doesn't work, check:
- The component's
sling:resourceType - The
sling:resourceSuperType - The model's
resourceType - The adapter type
- Whether the delegated model is available
Then check whether the model's resourceType and adapter configuration match what the component is actually using.
Again, understanding the runtime relationship is more useful than changing annotations randomly.
Why Architects Care About Sling Model Design
A Sling Model may look like a small Java class.
Across a large AEM implementation, however, thousands of models can exist.
Architecturally, the goal isn't to make every Sling Model small just for the sake of code size.
The goal is to give each layer a clear responsibility.
A Practical Rule
When designing a Sling Model, ask these questions:
-
What resource creates this model?
-
Which repository properties does the component need?
-
Which child resources are part of the component?
-
Does the model need request-specific context?
-
Does it need an OSGi service?
-
Is the logic presentation-related or business-related?
-
Can an existing Core Component model be reused?
-
Is delegation actually needed?
-
Would another developer understand the model's responsibility quickly?
If the answers are clear, the model usually becomes much easier to maintain.
Summary
Sling Models provide the bridge between AEM's resource-based content structure and the Java objects used by components.
The important things to remember are:
- A Sling Model is adapted from an object such as a Resource or request.
- Injectors determine where model fields get their values.
- Sling Models should prepare data for the component rather than contain large amounts of business logic.
- Reusable business rules belong in OSGi services.
- Core Component behavior can often be reused through Sling Model delegation.
sling:resourceSuperTypeand Java model delegation are related but separate mechanisms.
What's Next
We've now covered:
- How a resource becomes a Sling Model
- How adaptation works
- Where injection values come from
- Common Sling Model injectors
- OSGi service injection
- Resource super types
- Core Component model delegation
- The boundary between Sling Models and OSGi services
The next useful step is to look at Sling Model lifecycle, @PostConstruct, model initialization, and model delegation in more depth.
This is where many seemingly simple models start doing more work than expected, and understanding the lifecycle helps explain both common bugs and performance problems in real AEM projects.
Enjoyed this chapter?
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.