N
Naveenr.dev
Chapter 23
22 min read2026-08-14

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:

text
/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:

text
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:

java
@Model(adaptables = Resource.class)
public class ProductModel {

    @ValueMapValue
    private String title;
}
Sling Model Adaptation
Sling Model Adaptation

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:

java
/content/site/us/products/product-a/jcr:content/root/product

The resource can provide access to properties:

java
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:

text
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:

text
One object
   ↓
adaptTo(...)
   ↓
Another representation

For example:

java
PageManager pageManager =
    resource.getResourceResolver().adaptTo(PageManager.class);

The same idea is used with Sling Models:

java
ProductModel model =
    resource.adaptTo(ProductModel.class);

The Sling Models framework checks whether the requested model can be created from the adaptable object.

So:

java
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:

java
@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:

text
@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:

text
AEM Repository
      ↓
Resource
      ↓
Sling Model
      ↓
HTL

What Does @Model Actually Tell Sling?

Consider:

java
@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:

java
@Model(adaptables = Resource.class)

means the model can be created from a Resource:

text
Resource
   ↓
Sling Models Framework
   ↓
ProductModel

You can also encounter models adaptable from a Sling HTTP request:

java
@Model(adaptables = SlingHttpServletRequest.class)

In that case, the model is created from the request:

text
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.

java
@Model(adaptables = Resource.class)

A model that needs request-specific information may use:

java
@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

java
@Model(adaptables = Resource.class)
public class ProductModel {
}

The model starts with:

text
Resource

It can work with repository content and resource-related context.

Request-based model

java
@Model(adaptables = SlingHttpServletRequest.class)
public class ProductModel {
}

The model starts with:

java
SlingHttpServletRequest

Now request-specific information can be relevant.

For example:

text
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:

java
@ValueMapValue
private String title;

Suppose the component resource contains:

java
title = "Product A"

The model framework can obtain the resource's ValueMap and resolve the title property:

text
Resource
   │
   ▼
ValueMap
   │
   └── title
         │
         ▼
     @ValueMapValue
         │
         ▼
   ProductModel.title

Without Sling Model injection, we could read the property manually:

java
String title =
    resource.getValueMap().get("title", String.class);

With @ValueMapValue, the property can be injected directly into the model:

java
@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:

java
@ValueMapValue
private String title;

maps to:

text
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:

java
@ValueMapValue(name = "productTitle")
private String title;

Now the relationship is:

text
Repository property
    productTitle
         ↓
Java field
    title

This can be useful when the Java model uses a cleaner domain name than the underlying repository property.

java
@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:

java
@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:

text
title
  ↓
May be missing
  ↓
Model still usable

If a property is essential to the model:

text
productId
  ↓
Required
  ↓
Missing value should be treated as a content/model problem

The model can explicitly define its injection strategy when required:

java
@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.

text
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:

java
@ValueMapValue
private String imagePath;

If an author hasn't selected an image, the model can still exist.

The getter can handle the missing value:

java
public boolean hasImage() {
    return imagePath != null && !imagePath.isEmpty();
}

That is different from a component where a value is fundamental to its operation.

For example:

text
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.

text
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:

java
@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:

text
Reads component data
        ↓
Gets SKU
        ↓
Calls ProductService
        ↓
Exposes result to HTL

The service:

text
ProductService
        ↓
Business logic
        ↓
Repository / API / Pricing / Other Services

The model shouldn't become:

text
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:

text
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:

java
@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:

text
/content/site/us/products/product-a
    │
    └── jcr:content
          │
          └── product
                ├── sku = "ABC-123"
                └── title = "Product A"

The component has:

java
@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:

java
@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.

Injector Sources
Injector Sources

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:

text
title = "AEM Architecture"
description = "Understanding Sling Models"

The model can map those properties:

java
@ValueMapValue
private String title;

@ValueMapValue
private String description;

The source is the resource's ValueMap.

That's the important part.

When you see:

java
@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:

text
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:

java
@ChildResource
private ImageModel image;

This is different from:

java
@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:

text
product
├── title
├── sku
└── image
      ├── fileReference
      └── alt

A natural model structure is:

text
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:

java
@Self
private Resource resource;

If the model is adaptable from a Resource, @Self can provide the current adaptable.

Conceptually:

text
Current Adaptable
       │
       ▼
     @Self
       │
       ▼
    Resource

For example:

java
@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:

java
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:

java
@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:

text
Stored path
    ↓
Resource

This is different from @ValueMapValue, which simply reads the path string.

String Path vs Resource

Consider this property:

java
relatedProduct =
/content/site/us/products/product-b

With:

java
@ValueMapValue
private String relatedProduct;

you get the path itself:

java
"/content/site/us/products/product-b"

With an appropriate resource-path injection, the model can work with the actual resource:

java
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:

java
String

to:

java
Repository Resource

which is a different responsibility.

@OSGiService: Obtain a Backend Service

We've already seen this one in the previous section:

java
@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:

java
@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:

java
@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:

text
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:

java
@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:

java
@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:

text
                         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:

text
product
│
├── title
├── sku
│
├── image
│     ├── fileReference
│     └── alt
│
└── relatedProduct

A reasonable model might be:

java
@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:

text
Where does the value come from?
            │
     ┌──────┼─────────┬─────────────┐
     │      │         │             │
     ▼      ▼         ▼             ▼
 Property  Child    OSGi Service   Request /
           Resource                Context
     │      │         │             │
     ▼      ▼         ▼             ▼
 ValueMap Child     OSGiService  Appropriate
 Value    Resource               Context Injector

For example:

text
title
  ↓
Property
  ↓
@ValueMapValue
text
image
  ↓
Child resource
  ↓
@ChildResource
text
ProductService
  ↓
OSGi service
  ↓
@OSGiService
text
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:

java
@ValueMapValue
private String sku;

The injector's job is to obtain the SKU.

It shouldn't also:

text
Call commerce API
Calculate price
Validate customer
Apply discount
Check inventory

That belongs elsewhere.

The model can do:

text
Repository
    ↓
SKU
    ↓
ProductService
    ↓
Product Data

So the responsibilities remain:

text
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:

java
@ValueMapValue
private String title;

returns null.

Don't immediately assume the injector is broken.

Check the repository first.

text
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:

text
@ChildResource
      ↓
Does the child node/resource exist?

For an OSGi service:

text
@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:

java
@ValueMapValue
private String title;

The debugging path should be straightforward:

text
HTL
  ↓
ProductModel
  ↓
title = null
  ↓
What resource created the model?
  ↓
Inspect that resource
  ↓
Does "title" exist?

If the repository contains:

text
title = "Product A"

continue:

text
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:

text
                    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:

text
Image
├── Asset
├── Image URL
├── Alt Text
├── Width
├── Height
└── Responsive behavior

The project may only need to add one business-specific requirement.

For example:

text
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.

Sling Model Delegation
Sling Model Delegation

Why Delegation Exists

Imagine a Core Component already provides:

java
public interface Image {
    String getSrc();
    String getAlt();
    String getTitle();
}

Our project needs:

text
Existing Core Component behavior
+
Product-specific tracking information

We don't want to rewrite:

java
getSrc()
getAlt()
getTitle()

just to add:

java
getTrackingId()

The architecture we want is:

text
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:

java
@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:

java
@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:

java
/apps/myproject/components/image

and its resource super type points to a Core Component:

java
/apps/myproject/components/image
        │
        │ sling:resourceSuperType
        ▼
/libs/core/wcm/components/image/v3/image

Now Sling has an inheritance relationship:

text
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:

text
                    Project Model
                         │
             ┌───────────┴───────────┐
             │                       │
             ▼                       ▼
       Delegated methods        Custom methods
             │                       │
             ▼                       ▼
      Core Component          Project Logic

For example:

java
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

text
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

text
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:

text
Core Component
      ↓
Use it directly

No custom model is necessary.

If the project needs a small addition:

text
Core Component
      +
Small project requirement
      ↓
Delegation may be appropriate

If the project needs behavior that is fundamentally different:

text
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:

text
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:

java
<img
    src="${image.src}"
    alt="${image.alt}"
    data-tracking-id="${image.trackingId}"
/>

HTL doesn't need to know whether:

java
src

came from:

java
Core Component

or whether:

java
trackingId

came from:

java
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:

java
sling:resourceSuperType

which establishes component inheritance through Sling resource resolution.

At the Java level, our model may use delegation:

text
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:

text
Core Model
   ↓
Project Model

Then gradually adds:

text
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:

text
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:

java
public String getDisplayTitle() {
    return title != null ? title.trim() : "";
}

This can reasonably live in the model if it is purely presentation-related.

But:

java
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:

  1. The component's sling:resourceType
  2. The sling:resourceSuperType
  3. The model's resourceType
  4. The adapter type
  5. 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:

  1. What resource creates this model?

  2. Which repository properties does the component need?

  3. Which child resources are part of the component?

  4. Does the model need request-specific context?

  5. Does it need an OSGi service?

  6. Is the logic presentation-related or business-related?

  7. Can an existing Core Component model be reused?

  8. Is delegation actually needed?

  9. 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:resourceSuperType and 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.