Sling Models & Dependency Injection
Building Clean, Maintainable Components in AEM
In the previous article, we explored how Apache Sling resolves a URL into a Resource, identifies the appropriate component through `sling:resourceType`, and prepares the rendering pipeline.
At that point, Sling has answered an important question:
"Which component should render this content?"
The next question is equally important:
"Where does that component get the data it needs?"
That's where Sling Models come in.
They prepare presentation data, keep HTL templates clean, and provide one of the most elegant dependency injection mechanisms in the Java ecosystem. Once you understand Sling Models, component development in AEM becomes significantly simpler because each layer of the application has a clear responsibility.
Reading time: 10β12 min
--------------------------------
AEM Cloud Master Series
This article is Part 4 of a 10-part series designed to help frontend engineers and AEM developers understand Adobe Experience Manager from architecture to production deployment.
Series Roadmap
β Part 1 β Understanding Adobe Experience Manager Architecture
β Part 2 β The Complete AEM Request Lifecycle
β Part 3 β Inside Apache Sling
β Part 4 β Sling Models & Dependency Injection (Current)
Part 5 β OSGi Services Explained
Part 6 β HTL Explained
Part 7 β Dispatcher Deep Dive
Part 8 β Building Enterprise Components in AEM
Part 9 β React + Adobe AEM Cloud Service
Part 10 β From Development to Production
In this article, we'll focus on the presentation layer of AEM and explore how Sling Models prepare content for rendering, simplify dependency injection, and keep components clean as applications grow.
Why Sling Models Exist
When developers first start working with AEM, one question comes up quickly:
"Why can't I just read everything directly from HTL?"
Technically, you can read simple properties from the repository.
But as components become more complex, that approach quickly becomes difficult to maintain.
Imagine a product component that needs to:
Format dates
Build image URLs
Read child resources
Call Azure Search
Validate authored content
Hide incomplete data
Generate CTA links
None of that belongs in an HTML template.
Instead, AEM introduces a dedicated layer between the repository and the presentation.
That layer is the Sling Model.
Where Sling Models Fit
Let's revisit the rendering pipeline from the previous article.
Notice that the Sling Model doesn't replace the component.
It becomes part of the component.
Its job is to prepare everything HTL needs before rendering begins.
Separating Responsibilities
One of the design principles behind AEM is separation of concerns.
Each layer should have one clear responsibility.
| Layer | Responsibility |
|---|---|
| Repository | Store content |
| Sling | Resolve the request |
| Sling Model | Prepare data |
| OSGi Service | Execute reusable business logic |
| HTL | Render HTML |
Because each layer focuses on a single job, applications become easier to maintain, extend, and debug.
Before Sling Models
Imagine a Hero component with:
title
publish date
CTA text
Without a Sling Model, the template quickly starts accumulating formatting logic.
<h1>${properties.title}</h1>
<p>
${properties.publishDate}
</p>
Now imagine:
date formatting
localization
null handling
CTA validation
image optimization
The template becomes increasingly difficult to read.
With a Sling Model
Instead, the component asks the model for presentation-ready values.
@Model(adaptables = SlingHttpServletRequest.class)
public class HeroModel {
@ValueMapValue
private String title;
@ValueMapValue
private LocalDate publishDate;
public String getTitle() {
return title;
}
public String getFormattedDate() {
return publishDate.format(
DateTimeFormatter.ofPattern("MMMM d, yyyy")
);
}
}
HTL becomes much cleaner.
<sly data-sly-use.model="com.example.core.models.HeroModel"/>
<h1>${model.title}</h1>
<p>${model.formattedDate}</p>
Notice how the template doesn't know how the date was formatted.
It simply displays it.
What Does "Dependency Injection" Mean?
One of the biggest advantages of Sling Models is that they support Dependency Injection.
Instead of manually creating every object your component needs, Sling provides those objects automatically.
Think about checking into a hotel.
You don't build the room yourself.
You simply arrive, and everything you need is already there:
a bed,
electricity,
towels,
Wi-Fi.
Dependency Injection follows the same idea.
Instead of writing code to retrieve every dependency, you declare what you need, and Sling injects it for you.
The Simplest Example
Suppose your component needs the page title.
Instead of manually reading the repository, you simply declare:
@ValueMapValue
private String title;
Sling automatically finds the property and injects its value.
You don't write:
Resource resource = request.getResource();
ValueMap map = resource.getValueMap();
String title = map.get("title", String.class);
The framework handles that for you.
That's one of the reasons Sling Models remain concise even as components grow.
How Injection Works
At a high level, the lifecycle looks like this.
By the time HTL receives the model, everything has already been prepared.
The template simply renders the final values.
Adaptables: Where Does a Sling Model Get Its Data?
Every Sling Model starts with one important decision:
What is this model adapting from?
That's the purpose of the adaptables attribute.
@Model(adaptables = SlingHttpServletRequest.class)
public class HeroModel {
}
or
@Model(adaptables = Resource.class)
public class HeroModel {
}
Although these two declarations look similar, they provide access to different information.
Understanding when to use each one is an important part of writing clean Sling Models.
Resource vs SlingHttpServletRequest
A Resource represents the content being rendered.
A SlingHttpServletRequest represents the entire HTTP request.
Think of it this way:
| Adaptable | Gives you access to |
|---|---|
| Resource | Repository content |
| SlingHttpServletRequest | Resource + request + selectors + query parameters + current page context |
A Resource only knows about the content.
The request knows about everything happening during the current request.
Which One Should You Use?
If your component only reads authored content, adapting from a Resource is usually enough.
@Model(adaptables = Resource.class)
public class CardModel {
@ValueMapValue
private String title;
}
If your component needs information from the current request, adapt from SlingHttpServletRequest.
For example:
query parameters
selectors
request attributes
current page context
request-specific logic
@Model(adaptables = SlingHttpServletRequest.class)
public class SearchModel {
}
As a general rule:
Use the simplest adaptable that provides the data you need.
If you don't need the request, don't adapt from it.
The Most Common Injection Annotations
One of the biggest advantages of Sling Models is how little code you need to write.
Instead of manually looking up everything, you simply declare what you need.
Let's look at the annotations you'll use most often.
@ValueMapValue
This is probably the annotation you'll use more than any other.
It injects a property directly from the current resource.
Suppose the component has:
title = "Welcome"
description = "Modern workplace solutions"
The model becomes:
@ValueMapValue
private String title;
@ValueMapValue
private String description;
No repository lookups.
No ValueMaps.
No casting.
Just the property you requested.
@ChildResource
Sometimes data isn't stored as a simple property.
Instead, it's stored as a child node.
Imagine this structure.
hero
βββ title
βββ buttons
βββ button1
βββ button2
Instead of navigating manually, Sling injects the child resource.
@ChildResource
private Resource buttons;
This is especially useful for multifields and nested content structures.
@Self
Sometimes the object you need is the adaptable itself.
For example:
@Self
private SlingHttpServletRequest request;
or
@Self
private Resource resource;
Instead of calling helper methods repeatedly, Sling injects the current object directly.
@OSGiService
Sling Models shouldn't contain business logic.
Instead, they delegate reusable work to services.
@OSGiService
private SearchService searchService;
Now the model can simply call:
searchService.search(query);
without worrying about how the service is created.
How Injection Happens
A simplified lifecycle looks like this.
By the time HTL starts rendering, every injected dependency is already available.
That's why templates remain so clean.
@PostConstruct
Sometimes injected values aren't enough.
You may need to:
format data
combine multiple fields
filter results
initialize collections
Instead of doing that in every getter, Sling provides the @PostConstruct annotation.
@PostConstruct
protected void init() {
formattedTitle = title.toUpperCase();
}
The method runs automatically after dependency injection finishes and before HTL uses the model.
Think of it as the model's initialization step.
It's the ideal place for setup work that only needs to happen once.
Production Tip
One mistake I see fairly often is putting too much work inside @PostConstruct.
It's tempting to fetch external data, execute complex business logic, or perform expensive calculations there because it runs automatically.
In practice, @PostConstruct should focus on preparing the model, not implementing your application's business layer.
If the initialization starts growing beyond simple formatting or composition, it's usually a sign that the work belongs in an OSGi Service instead.
Keeping @PostConstruct lightweight makes models easier to understand, easier to test, and faster to execute.
Real-World Example
Let's build a slightly more realistic example.
Imagine you're creating a Product Card component.
The author enters:
Product name
Description
Image
Product ID
The actual price, however, comes from an external pricing service.
The responsibilities are split like this:
JCR stores the authored content.
Sling Model combines the authored content with live pricing.
OSGi Service communicates with the pricing API.
HTL renders the final HTML.
This separation allows each part of the application to evolve independently.
The marketing team can update product descriptions without changing Java code, while developers can update the pricing service without touching the HTL template.
Designing Good Sling Models
A Sling Model should have one clear responsibility:
Prepare data for a single component.
If you find yourself writing hundreds of lines of business logic inside a model, it's usually a sign that some of that work belongs in an OSGi Service.
A good Sling Model typically:
reads authored content,
performs simple formatting,
combines related values,
delegates reusable business logic,
exposes clean getter methods.
It should not become the business layer of your application.
Keeping HTL Simple
One of the easiest ways to evaluate a component is to look at its HTL file.
If the template contains lots of conditional logic, formatting, or repository access, the Sling Model probably isn't doing enough.
Instead, aim for templates that read almost like plain HTML.
<sly data-sly-use.model="com.example.core.models.ProductModel"/>
<article class="product-card">
<img src="${model.image}" alt="${model.altText}" />
<h2>${model.title}</h2>
<p>${model.description}</p>
<span class="price">${model.price}</span>
</article>
The template doesn't need to know:
where the price came from,
how the image URL was generated,
whether a fallback value was used.
Its only responsibility is rendering.
Common Mistakes
After reviewing many AEM codebases, a few patterns appear repeatedly.
Using Sling Models as Service Classes
Sometimes developers place all application logic inside a Sling Model.
The model starts handling:
API communication
business rules
validation
caching
calculations
At that point, it has stopped being a presentation model.
If multiple components could reuse the logic, move it into an OSGi Service instead.
Putting Logic in HTL
Another common mistake is treating HTL like a programming language.
Templates shouldn't contain complicated conditions or formatting logic.
If you're writing complex expressions inside HTL, ask yourself:
Could this be prepared in the Sling Model instead?
Most of the time, the answer is yes.
Choosing the Wrong Adaptable
Developers often default to:
SlingHttpServletRequest.class
for every model.
Sometimes that's necessary.
Often it isn't.
If your model only needs repository content, adapting from Resource.class is simpler and makes the intent of the model clearer.
Forgetting Null Safety
Content authors don't always complete every field.
Models should expect that.
Handle missing values gracefully and provide sensible defaults where appropriate.
A component that continues rendering with partial content is usually better than one that fails because a single property wasn't authored.
Sling Model Lifecycle
Understanding the lifecycle helps explain why dependency injection feels so seamless.
By the time HTL accesses the model, initialization has already completed.
Everything needed for rendering should already be available.
Mental Model
One analogy I like is thinking of a Sling Model as a chef.
The JCR is the pantry.
The OSGi Service is a supplier that can bring in extra ingredients.
The Sling Model prepares the meal.
HTL is the waiter serving it to the customer.
You wouldn't ask the waiter to cook dinner.
Likewise, you shouldn't ask HTL to prepare your data.
Each layer has its own responsibility.
Once you start treating Sling Models as the presentation layerβnot the business layerβyou'll naturally write components that are easier to read, easier to test, and much easier to maintain as your AEM application grows.
Debugging Sling Models
One of the biggest advantages of understanding Sling Models is that debugging becomes much more systematic.
When a component doesn't render correctly, resist the temptation to immediately inspect the HTL template.
Instead, work through the model layer step by step.
Ask yourself:
Was the correct model instantiated?
Were all dependencies injected successfully?
Did
@PostConstructcomplete without errors?Did the OSGi Service return valid data?
Is HTL rendering the expected values?
Following the same sequence every time helps isolate problems much faster.
Sling Model Cheat Sheet
| Annotation | Purpose | Typical Use |
|---|---|---|
@Model | Declares a Sling Model | Every model class |
@ValueMapValue | Injects a property from the current resource | Text fields, numbers, booleans |
@ChildResource | Injects a child resource | Multifields, nested content |
@Self | Injects the adaptable itself | Current Resource or Request |
@OSGiService | Injects an OSGi Service | Business logic, APIs |
@PostConstruct | Runs initialization after injection | Formatting, preparation |
Choosing the Right Layer
One question I often ask myself while building components is:
"Does this code prepare data, or does it perform business logic?"
That answer usually tells me where the code belongs.
| If your code... | It belongs in... |
|---|---|
| Reads authored properties | Sling Model |
| Formats data for display | Sling Model |
| Combines multiple values | Sling Model |
| Calls external APIs | OSGi Service |
| Contains reusable business rules | OSGi Service |
| Generates HTML | HTL |
Keeping these responsibilities separate results in components that are easier to understand, easier to test, and much easier to maintain over time.
Final Thoughts
Sling Models are one of the features that make AEM development enjoyable once you become comfortable with them.
Instead of filling templates with business logic or writing repetitive repository access code, you describe the data your component needs and let the framework provide it.
That approach leads to components that are:
easier to read,
easier to reuse,
easier to debug,
and easier to test.
More importantly, it encourages a clean separation between presentation and business logic, which becomes increasingly valuable as projects grow.
When combined with OSGi Services, Sling Models provide a simple but powerful architecture: models prepare data, services perform reusable work, and HTL focuses entirely on presentation.
Key Takeaways
Sling Models prepare presentation dataβthey don't replace business services.
Dependency Injection eliminates repetitive lookup code and keeps models concise.
Choose the simplest adaptable (
ResourceorSlingHttpServletRequest) that satisfies your component's needs.Use
@ValueMapValue,@ChildResource,@Self, and@OSGiServiceto express dependencies instead of retrieving them manually.Reserve
@PostConstructfor lightweight initialization and data preparation.Keep HTL free of business logic by exposing presentation-ready values from the model.
A well-designed Sling Model should be easy to understand at a glance. If your model starts growing into hundreds of lines of code or handling multiple responsibilities, it's usually a sign that some of that logic belongs elsewhere. Keeping each layer focused on its own responsibility is one of the practices that separates maintainable enterprise AEM applications from codebases that become difficult to evolve over time.
Continue Reading
You've now seen how Sling Models prepare presentation-ready data, simplify dependency injection, and keep presentation logic separate from reusable business services.
In the next article, we'll move beyond individual components and explore OSGi Services, the reusable business layer that powers enterprise AEM applications. You'll learn how services communicate with external systems, encapsulate shared logic, and help keep Sling Models focused on preparing data for presentation.
Next Article β Part 5: OSGi Services Explained
Masoud
August 22nd, 2025