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.

LayerResponsibility
RepositoryStore content
SlingResolve the request
Sling ModelPrepare data
OSGi ServiceExecute reusable business logic
HTLRender 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:

AdaptableGives you access to
ResourceRepository content
SlingHttpServletRequestResource + 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 @PostConstruct complete 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

AnnotationPurposeTypical Use
@ModelDeclares a Sling ModelEvery model class
@ValueMapValueInjects a property from the current resourceText fields, numbers, booleans
@ChildResourceInjects a child resourceMultifields, nested content
@SelfInjects the adaptable itselfCurrent Resource or Request
@OSGiServiceInjects an OSGi ServiceBusiness logic, APIs
@PostConstructRuns initialization after injectionFormatting, 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 propertiesSling Model
Formats data for displaySling Model
Combines multiple valuesSling Model
Calls external APIsOSGi Service
Contains reusable business rulesOSGi Service
Generates HTMLHTL

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 (Resource or SlingHttpServletRequest) that satisfies your component's needs.

  • Use @ValueMapValue, @ChildResource, @Self, and @OSGiService to express dependencies instead of retrieving them manually.

  • Reserve @PostConstruct for 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