Sling Model
Before we start — this is the file that matters most Five of your syllabus points land in this one topic. That is not a coincidence. Sling Models are where AEM development actually happens.
Components are folders. Templates are configuration. Clientlibs are CSS. But the Sling Model is where you write Java, and it is where an interviewer finds out whether you can actually code or have only assembled things.
That also makes this the file to slow down on. Every other AEM topic can be learned by reading. This one needs the Java underneath to be solid — because the questions here are genuinely Java questions wearing AEM clothes:
"Why @PostConstruct instead of a constructor?" → that is an object lifecycle question. "Why @ValueMapValue instead of @Inject?" → that is a dependency injection question. "Why can't you use @Reference in a model?" → that is a question about who creates the object. So this file explains the Java reasoning first and the AEM syntax second. If a concept feels shaky, it is worth rereading rather than moving on — every later file assumes this one.
- Introduction 1.1 The problem Sling Models solve Let's start with what the code looked like before them, because the improvement is the point.
Say you want a component's title and description from the repository. Without Sling Models, that is:
// The old way. Do not write this. Node node = resource.adaptTo(Node.class); String title = null; String description = null; try { if (node.hasProperty("jcr:title")) { title = node.getProperty("jcr:title").getString(); } if (node.hasProperty("description")) { description = node.getProperty("description").getString(); } } catch (RepositoryException e) { // ... and now what? }Copy Copy Look at what is wrong there. Twelve lines to read two values. A checked exception you cannot meaningfully handle. Manual hasProperty checks. Property names as raw strings scattered through the method. And it is nearly impossible to unit test, because you need a JCR session.
Now the same thing with a Sling Model:
@Model(adaptables = Resource.class) public class TitleModel {
}Copy Copy The values arrive by themselves. No lookups, no exceptions, no null-checking ceremony.
That is the whole idea, and it is the first thing to say in an interview:
"A Sling Model is a plain Java class that Sling populates for you. You declare fields, annotate them to say where the values come from, and Sling fills them in when the model is created. It replaced a lot of boilerplate JCR API code that was verbose, exception-heavy and hard to test."
1.2 What "adapting" means — the concept underneath Before the annotations, one concept has to be solid, because the whole mechanism sits on it.
Sling has an idea called adaptation. It means: "I have an object of one type. Give me a different type built from it."
The method is adaptTo(), and you have seen it already:
resource.adaptTo(Node.class); // Resource → JCR Node resource.adaptTo(ValueMap.class); // Resource → a map of its properties resource.adaptTo(Page.class); // Resource → an AEM Page resource.adaptTo(CtaModel.class); // Resource → YOUR modelCopy Copy That last line is the one that matters. A Sling Model is just another thing a Resource can be adapted into. You are extending Sling's adaptation system with your own types.
A useful analogy. Think of a Resource as a raw ingredient — say, a repository node. Adaptation is asking a kitchen to turn it into a specific dish. Sling ships with several recipes built in (Node, ValueMap, Page). A Sling Model is you handing the kitchen your own recipe.
The one thing to remember about adaptTo(), forever:
It can return null.
If there is no recipe for the conversion, or the recipe fails, you get null — not an exception. This is the single largest source of NullPointerExceptions in AEM code, and it is why every adaptTo in this repository is null-checked.
1.3 What actually is an annotation? If your Java is rusty, this is worth thirty seconds, because the whole file is annotations.
An annotation like @ValueMapValue is not code that runs. It is a label attached to a field. On its own it does nothing at all.
What makes it work is that something else reads those labels. In our case, the Sling Models framework inspects your class, sees the labels, and acts on them.
@ValueMapValue ← a label saying "fill this from the ValueMap" private String title; ← the field the label is attached toCopy Copy So the flow is: you attach labels → Sling reads them → Sling does the work. You are not calling anything. You are describing what you want, and a framework is doing the fetching.
That mental model matters, because it explains several behaviours later — particularly why @PostConstruct exists at all.
1.4 A real project example to adapt "On our site every component has a Sling Model in the core bundle, and the HTL just calls it with data-sly-use. We adapt from Resource by default and only from the request when we genuinely need something request-scoped, like the current page or the resolved policy. Models expose an interface with the implementation in a non-exported impl package, following the Core Components pattern, and each one is unit tested with AEM Mocks — mostly against the cases authors actually create, like a half-filled component."
That covers the layering, the adaptables decision, the interface pattern and testing — four likely follow-ups, pre-empted.
- Core Concepts 2.1 How to turn a Java class into a Sling Model (syllabus point 6) Your syllabus asks this directly. There are four steps, and step 2 is the one people forget.
Step 1 — Annotate the class with @Model.
@Model(adaptables = Resource.class) public class CtaModel { }Copy Copy adaptables is mandatory. It says what this model can be built from.
Step 2 — Make sure the package is scanned.
This is the step that causes "my model returns null and I have no idea why."
Sling does not scan every class in every bundle — that would be slow. Your bundle has to declare which packages contain models. Classically that is a bundle header:
Sling-Model-Packages: com.energy.core.modelsCopy Copy In newer AEM archetypes using the bnd plugin, this is generated automatically from the @Model annotation, so you often do not write it by hand. But you must know it exists, because when a model mysteriously will not adapt, this is a prime suspect.
How to check: open /system/console/status-adapters. If your model is registered, its adapter appears there. If it does not appear, the package is not being scanned — which is a much faster diagnosis than staring at the class.
Step 3 — Declare fields with injector annotations.
@ValueMapValue private String linkText;Copy Copy Step 4 — Add getters.
public String getLinkText() { return linkText; }Copy Copy Getters matter because HTL can only call getters. In HTL, ${cta.linkText} calls getLinkText(). A public field will not work — HTL follows the JavaBeans convention.
Then use it. From HTL:
CtaModel model = resource.adaptTo(CtaModel.class); if (model != null) { // ALWAYS null-check String text = model.getLinkText(); }Copy Copy The interview answer:
"Four steps. Annotate the class with @Model and declare what it adapts from. Make sure the package is registered for model scanning — classically the Sling-Model-Packages bundle header, though newer archetypes generate that from the annotation. Declare fields with injector-specific annotations like @ValueMapValue. And add getters, because HTL follows the JavaBeans convention and can only reach values through a getter.
The step that trips people up is the package scanning, because when it's wrong the model just silently returns null. I check /system/console/status-adapters first — if the adapter isn't registered there, it's a scanning problem, not a code problem."
2.2 adaptables — what it means (syllabus point 6) @Model(adaptables = Resource.class)Copy Copy adaptables declares what types this model can be built from.
Read it as: "you can call .adaptTo(CtaModel.class) on a Resource, and you'll get one of these."
You can list more than one:
@Model(adaptables = {Resource.class, SlingHttpServletRequest.class})Copy Copy Now both a Resource and a request can produce this model. Useful for a model that mostly needs content but occasionally needs request data.
What happens if you get it wrong: you call adaptTo on a request, but the model only declares Resource.class, and you get null. No exception, no message. Silent null. This is the second most common cause of "my model is null," after package scanning.
2.3 Resource.class versus SlingHttpServletRequest.class (syllabus point 6 — asked explicitly) Your syllabus asks when to use each, which means an interviewer asked exactly that. Here is the reasoning rather than a rule to memorise.
The difference in one line:
A Resource is a piece of content. A request is a visit to that content by a particular person, with particular parameters, on a particular page.
So the question becomes: does your model need to know anything about the visit, or only about the content?
Use Resource.class when the model only needs content. Titles, descriptions, links, child nodes — anything stored on the node.
Use SlingHttpServletRequest.class when you need something that only exists during a request. Specifically:
You need Only available from a request currentPage — the page being rendered Yes currentStyle — the resolved policy (from file 03) Yes pageManager Yes wcmmode — is this edit mode? Yes Selectors, suffix, request parameters Yes Request attributes set by a filter Yes The request or response object itself Yes Why Resource.class should be your default — three genuine reasons, and giving all three is what makes this answer strong:
One — it is easier to unit test. A test can create a Resource from a JSON fixture in one line. Mocking a full request is more setup.
Two — it is reusable outside a request. A scheduled job, a workflow step or a servlet iterating over search results has resources but no page-rendering request. A request-adaptable model simply cannot be used there.
Three — it is honest about dependencies. Declaring Resource.class says "this model depends only on content." That is a smaller, clearer contract.
A detail that impresses: when you adapt from a request, @ValueMapValue and @ChildResource still work — they resolve against the request's resource. So going request-adaptable does not cost you the content injections. You are adding capability, not swapping it.
The interview answer:
"The question I ask is whether the model needs to know anything about the visit, or only about the content.
If it only needs content — titles, links, child nodes — I adapt from Resource. That's my default for three reasons: it's easier to unit test, since a test can build a resource from a JSON fixture without mocking a request; it's reusable outside a request, so a scheduler or workflow step can use the same model; and it declares a smaller dependency, which is honest about what the model actually needs.
I adapt from SlingHttpServletRequest when I need something that only exists during a request — currentPage, currentStyle for the resolved policy, wcmmode to check for edit mode, or selectors and request parameters. Anything injected with @ScriptVariable needs the request, because those come from the HTL bindings.
Worth knowing: adapting from the request doesn't lose you the content injections. @ValueMapValue and @ChildResource still resolve against the request's resource. So it's additive."
A concrete example from our project makes it land:
// Resource is enough -- this only reads content @Model(adaptables = Resource.class) public class CtaModel { ... }
// Request is required -- currentStyle is the resolved POLICY, // which only exists in a rendering context @Model(adaptables = SlingHttpServletRequest.class) public class PageModel { @ScriptVariable private Style currentStyle; // needs the request }Copy Copy 2.4 The annotations — the full map (syllabus point 6) Your syllabus asks "what annotations are used in a model class." Rather than a flat list, here they are grouped by where the value comes from, which is how you should think about them.
Group 1 — reading content
Annotation Gets you Typical use @ValueMapValue A property on this node Title, description, a link path @ChildResource A child node, or a list of them Composite multifield rows, nested config @ResourcePath A Resource, from a path stored in a property Follow a fileReference to the asset Group 2 — reaching outside the content
Annotation Gets you Typical use @OSGiService An OSGi service Call a shared service from the model @ScriptVariable An HTL binding currentPage, currentStyle, pageManager, wcmmode @RequestAttribute A request attribute A value set upstream by a filter @SlingObject A common Sling object resourceResolver, resource, request @Self The adaptable itself Adapt this model into another model Group 3 — modifiers (they change how another injection behaves)
Annotation Effect @Named("propertyName") The property name differs from the field name @Default(values = "...") Supply a fallback when nothing is found @Optional / @Required Override the injection strategy for one field @Via("resource") Change what the injection resolves against @Filter("(...)") Narrow an @OSGiService to a specific implementation Group 4 — lifecycle
Annotation Effect @PostConstruct Run a method after injection completes — section 2.7 Group 5 — the generic one
Annotation Effect @Inject Try every injector until one returns something — section 2.6 If you learn five, learn these: @ValueMapValue, @ChildResource, @OSGiService, @ScriptVariable, @PostConstruct. Those cover the overwhelming majority of real model code, and four of them are in your syllabus.
2.5 @ValueMapValue — reading a property (syllabus point 9) The workhorse. It reads a property from the node's ValueMap.
@ValueMapValue private String linkText; // reads the "linkText" propertyCopy Copy By default the field name is the property name. If they differ, say so:
@ValueMapValue @Named("jcr:title") private String title; // field "title", property "jcr:title"Copy Copy That @Named case comes up constantly, because JCR property names contain colons and a Java field cannot.
Type conversion is automatic:
@ValueMapValue private String title; // String property
@ValueMapValue private boolean newTab; // Boolean property
@ValueMapValue private Integer maxItems; // Long in JCR → Integer in Java
@ValueMapValue private String[] tags; // multi-value property → array
@ValueMapValue private Calendar publishDate; // Date propertyCopy Copy A subtlety worth knowing: prefer Integer over int for numbers. A primitive int cannot be null, so a missing property becomes 0, which is indistinguishable from an author genuinely entering zero. With Integer you get null and can tell the difference. Same reasoning for Boolean versus boolean when "not set" and "false" mean different things — which, as file 02 showed, is exactly the checkbox uncheckedValue situation.
Defaults:
@ValueMapValue @Default(values = "arrow") private String style;Copy Copy 2.6 @Inject versus @ValueMapValue (syllabus point 9) Your syllabus lists these together, which means the interviewer compared them. Here is the real answer.
@Inject is the generic annotation. It does not say where the value comes from. Sling tries every registered injector in ranking order until one returns something:
script bindings → value map → child resources → request attributes → OSGi services → resource path → self → sling object → ...Copy Copy @ValueMapValue is injector-specific. It goes straight to the ValueMap. Nothing else is tried.
Three reasons injector-specific wins:
One — it is unambiguous. Reading @ValueMapValue private String title; tells you exactly where that value comes from. Reading @Inject private String title; tells you nothing — you have to know the injector ranking to guess.
Two — it is faster. @Inject may try several injectors before one succeeds. On a page with fifty component instances, that adds up.
Three — and this is the real danger — @Inject can pick up the wrong thing.
Script bindings are tried before the ValueMap. So if you write:
@Inject private String currentPage; // you meant a property called "currentPage"Copy Copy you get the HTL binding currentPage — an AEM Page object — not your property. And it fails in a confusing way, because the types do not match.
That class of bug is genuinely hard to find, and it disappears entirely if you are specific.
The interview answer:
"@Inject is generic — it tries every registered injector in ranking order until one returns a value. @ValueMapValue is injector-specific and goes straight to the resource's ValueMap.
I always use the injector-specific ones. Three reasons. It's self-documenting — you can see where a value comes from just by reading the field. It's faster, because @Inject may try several injectors first, which matters on a page with many component instances. And most importantly it avoids a real class of bug: script bindings are tried before the ValueMap, so an @Inject field named the same as an HTL binding silently picks up the binding instead of your property.
@Inject isn't wrong exactly — it's older and it works — but on a project you're maintaining, being explicit is worth a lot."
Being able to say @Inject is legacy but functional, rather than just "wrong," is the more senior answer.
2.7 @PostConstruct — why it exists (syllabus point 8) Your syllabus asks this directly. It is a Java lifecycle question, so let's build it properly.
Start with the natural question. In normal Java, you initialise an object in its constructor:
public class Normal { private String greeting;
}Copy Copy So why can't a Sling Model do its setup in a constructor?
Because of the order things happen in.
graph LR
A["1. Sling creates
the instance
CONSTRUCTOR RUNS
fields are still null"] --> B["2. Sling INJECTS
values into fields
now the fields
have values"]
B --> C["3. Sling calls the
@PostConstruct method
safe to use
the fields"]
⛶ Fullscreen
The constructor runs before injection. So inside a constructor, every injected field is still null. Any setup that depends on injected values would fail there.
@PostConstruct is the hook that runs after injection. Think of it as "the constructor that runs at the right time."
@Model(adaptables = Resource.class) public class CtaModel {
}Copy Copy Now — what do you actually use it for? Three things, and the second is the one interviewers like.
Use 1 — derive values from injected fields. As above.
Use 2 — do expensive work exactly once.
This is the important one, and it comes straight from file 02.
HTL can call a getter many times. Inside a data-sly-list over fifty items, a getter called in the loop body runs fifty times. If that getter does a repository lookup or builds a list, you have just done it fifty times.
// BAD -- runs on every single call
public List
// GOOD -- runs exactly once
private List
@PostConstruct protected void init() { this.cards = buildCardList(); }
public List
This one surprises people. If @PostConstruct throws an exception, adaptTo returns null.
So you can use it as a gate:
@PostConstruct protected void init() throws IllegalStateException { if (StringUtils.isBlank(rootPath)) { throw new IllegalStateException("rootPath is required"); } // ... }Copy Copy Now a misconfigured component gives you null rather than a half-built model. Whether that is a good idea depends — usually returning a valid model with an isReady() flag is friendlier for HTL. But knowing the behaviour exists is worth a mark.
The rules:
The method must return void and take no arguments. Make it protected or private — it is not part of your public API. Name it whatever you like; init is conventional. In an inheritance chain, the parent's @PostConstruct runs first. The interview answer:
"Because injection happens after the object is constructed. Sling creates the instance first — so inside the constructor every injected field is still null — then injects the values, and only then calls @PostConstruct. So it's effectively the constructor that runs at the right time, once the injected values are actually available.
I use it for three things. Deriving values from injected fields, which can't be done in a constructor. Doing expensive work exactly once — that's the important one, because HTL can call a getter many times, and a getter inside a data-sly-list over fifty items runs fifty times. So I build lists and do lookups in @PostConstruct and store the result in a field.
And third, validation — if @PostConstruct throws, adaptTo returns null, so you can make a misconfigured model refuse to exist. Though in practice I usually prefer returning a valid model with an isReady() flag, because it gives HTL something cleaner to test against.
The method has to be void with no arguments, and I keep it protected since it isn't public API."
2.8 @OSGiService — calling a service from a model (syllabus point 10) Your syllabus asks: "which annotation is used to call a service class in a model?"
The answer is @OSGiService.
@Model(adaptables = Resource.class) public class ProductModel {
}Copy Copy And now the follow-up that always comes: "why not @Reference?"
This is a good question and the answer shows whether you understand OSGi.
"@Reference is an OSGi Declarative Services annotation, and it only works inside an OSGi component — a class annotated with @Component. Those are created and managed by the DS runtime, which is what wires the references.
A Sling Model isn't an OSGi component. It's created by the Sling Models framework, freshly, each time something adapts to it. The DS runtime never sees it, so it can't inject anything into it.
So the rule is: @Reference inside an @Component, @OSGiService inside a @Model. Same idea — get me a service — but two different frameworks doing the injecting."
That distinction is exactly your syllabus points 10 and 11 side by side, and here they are as a table:
Inside a Sling Model Inside an OSGi component Annotation @OSGiService @Reference Framework doing the injecting Sling Models OSGi Declarative Services Applies to classes marked @Model @Component Examples Component models Services, servlets, filters, schedulers Picking a specific implementation. If several implementations of an interface are registered, narrow it with a filter:
@OSGiService(filter = "(component.name=com.energy.core.services.impl.CachedProductDataService)") private ProductDataService productDataService;Copy Copy One important caution. Injecting a service into a model is easy, which makes it easy to abuse. A model that calls an external API synchronously during rendering blocks the page. Keep the service call cheap, or move the fetch client-side — the caching argument from file 02 applies directly.
2.9 @ChildResource — reading child nodes (syllabus point 12) Your syllabus asks why we use it. The answer is easiest as a contrast:
@ValueMapValue reads a property on this node. @ChildResource reads a child node of it.
Recall the composite multifield from file 02:
faqs/ ├── item0/ │ ├── question = "What is a transformer in electricity?" │ └── answer = "
It changes the voltage level...
" └── item1/ ├── question = "What do electrical transformers do?" └── answer = "They transfer energy between circuits...
"Copy Copy question and answer are not properties on the component node. They are properties on child nodes. @ValueMapValue cannot see them.Three ways to use @ChildResource:
As a raw Resource:
@ChildResource private Resource image; // the child node called "image"Copy Copy Adapted into another model — much more useful:
@ChildResource private LinkModel link; // child node "link", adapted to LinkModelCopy Copy As a list — this is the multifield case:
@ChildResource
private List
The interview answer:
"@ValueMapValue reads a property on the current node; @ChildResource reads a child node. The main reason I use it is composite multifields — those store each row as a child node like faqs/item0 rather than as properties on the component, so @ValueMapValue can't see them at all. I inject List
It's also useful for any structured sub-node — a nested configuration block, or an image node with several properties that deserves its own small model rather than five loose fields on the parent."
2.10 defaultInjectionStrategy — OPTIONAL versus REQUIRED A small setting with large consequences.
@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL )Copy Copy REQUIRED is the default if you say nothing. It means: if any injection fails to find a value, the whole model fails and adaptTo returns null.
OPTIONAL means: a missing value is just null, and the model is still created.
For components, OPTIONAL is almost always right, because authors leave fields empty constantly. With REQUIRED, one blank optional field makes the entire component vanish.
But state the trade-off, because interviewers ask:
"OPTIONAL hides mistakes. If I typo a property name, I get null instead of an error — the model builds fine and the value is silently missing. So OPTIONAL is right for component models, since authors leave fields blank all the time and I'd rather render partially than not at all, but it does mean a typo shows up as an empty page rather than a stack trace. That's why 'my model is returning null' is such a common debugging question — and also why @Required on genuinely mandatory fields is worth using."
Per-field override:
@ValueMapValue(injectionStrategy = InjectionStrategy.REQUIRED) private String rootPath; // this one really is mandatoryCopy Copy 2.11 Interfaces, adapters and resourceType — the professional pattern Worth knowing, because it is how Core Components are built and it comes up in senior rounds.
Instead of one class, you write an interface (the public contract) and an implementation (internal):
// Exported package -- this is the API public interface Cta { String getLinkText(); String getLinkUrl(); }Copy Copy // NON-exported .impl package @Model( adaptables = Resource.class, adapters = Cta.class, // register AS the interface resourceType = "energy/components/cta", // bind to a resource type defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class CtaImpl implements Cta { // ... }Copy Copy Now HTL uses the interface:
One — you can change the implementation without touching HTL or anything else that depends on the interface.
Two — resourceType binding. With it set, Sling can pick the right model based on the resource's type. That matters when several implementations share one interface, and it is required for the Sling Model Exporter (file 17).
Three — module boundaries. The interface lives in an exported package; the implementation lives in an .impl package that is not exported. That is proper OSGi hygiene, from file 01.
-
Internal Working 3.1 The full lifecycle of a model sequenceDiagram participant H as HTL participant S as Sling Models framework participant I as Injectors participant M as Your model instance
H->>S: data-sly-use.cta="com.energy...CtaModel" S->>S: is this class registered? (package scanned?) S->>S: does it declare this adaptable type? S->>M: create the instance (CONSTRUCTOR — fields null) S->>I: for each annotated field, ask the right injector I->>M: set linkText I->>M: set linkUrl I->>M: set style S->>S: any REQUIRED injection fail? S->>M: call @PostConstruct M->>M: derive values, build lists ONCE S-->>H: the ready model H->>M: ${cta.linkText} → calls getLinkText() ⛶ Fullscreen Four points to draw out of that:
Registration is checked first. If the package is not scanned, nothing else happens — you get null immediately.
The constructor runs before injection. That is @PostConstruct's entire reason for existing.
A failed REQUIRED injection kills the whole model, returning null.
Getters are called last, and possibly many times. That is why expensive work belongs in @PostConstruct.
3.2 Why a model returns null — the five causes "My Sling Model is null" is one of the most common AEM debugging questions. There are exactly five reasons, and knowing them in order is the answer:
graph TD
A["adaptTo returned null"] --> B{"Is the model registered?
/system/console/status-adapters"}
B -->|No| C["1. PACKAGE NOT SCANNED
Sling-Model-Packages header"]
B -->|Yes| D{"Does adaptables match
what you adapted from?"}
D -->|No| E["2. WRONG ADAPTABLE
e.g. adapting a request to a
Resource-only model"]
D -->|Yes| F{"Any REQUIRED injection
that found nothing?"}
F -->|Yes| G["3. REQUIRED INJECTION FAILED
use OPTIONAL, or fix the property"]
F -->|No| H{"Did @PostConstruct throw?"}
H -->|Yes| I["4. POSTCONSTRUCT THREW
check error.log"]
H -->|No| J["5. RESOURCE TYPE MISMATCH
if the model is resourceType-bound"]
⛶ Fullscreen
Being able to list those five in order is a genuinely strong answer, because it turns a vague symptom into a checklist.
3.3 Injector ranking — why @Inject is risky Sling's injectors are tried in a defined order. Roughly:
- script-bindings ← currentPage, currentStyle, wcmmode, pageManager
- value-map ← node properties
- child-resources ← child nodes
- request-attributes
- osgi-services
- resource-path
- self
- sling-objectCopy Copy Notice that script bindings come first. That is why @Inject private String currentPage; gets you the HTL binding rather than a property of that name — the bindings injector answers before the ValueMap ever gets asked.
Injector-specific annotations skip this entire list and go directly to one injector. That is the whole argument for using them.
3.4 @Model(cache = true) By default, adapting the same resource twice creates two separate instances, and any @PostConstruct work runs twice.
@Model(adaptables = Resource.class, cache = true)Copy Copy With caching on, the instance is reused for the same adaptable within the same request.
When it helps: a model doing genuinely expensive work in @PostConstruct that gets adapted from more than one place during a render.
When to leave it off: simple models. The caching has its own small cost, and for a model that just reads three properties it is not worth it.
4 sections hidden in Learn mode 4. Important Interview Questions · 5. Cross Questions — how this topic gets drilled · 6. Best Interview Answers · 7. Real Project Examples Show 8. Coding Examples Building on the components from file 02, at the three levels.
8.1 Simple — the CTA model, annotated line by line package com.energy.core.models;
import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import javax.annotation.PostConstruct;
@Model( // Only needs CONTENT, so Resource is enough. // Easier to test, and reusable from a servlet or scheduler. adaptables = Resource.class,
) public class CtaModel {
}Copy Copy The five decisions to be able to defend:
adaptables = Resource.class — content only, so keep the dependency small.
OPTIONAL — because authors leave fields blank, and a vanished component is worse than a partial one.
Boolean not boolean — so "not set" and "false" stay distinguishable.
@PostConstruct for the derived values — impossible in a constructor, and it runs once.
Getters never return null for text, so HTL never renders the literal string "null". The exception is getTarget() and getRel(), where null is deliberate — HTL omits the whole attribute rather than writing target="".
8.2 Medium — @ChildResource for a composite multifield This is syllabus point 12 in practice, reading the FAQ accordion from file 02.
The nested row model — one per multifield row:
package com.energy.core.models;
import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class FaqItemModel {
}Copy Copy The parent model — this is where @ChildResource does its work:
package com.energy.core.models;
import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.ChildResource; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import javax.annotation.PostConstruct; import java.util.Collections; import java.util.List; import java.util.stream.Collectors;
@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class FaqModel {
}Copy Copy The three points to make about this pair:
@ChildResource on a List
The injected list can be null if the author never added a row. That is why @PostConstruct normalises it and the getter returns an empty list.
Filtering happens once in @PostConstruct, not in the getter — the same lesson as story 1.
8.3 Complex — request-adaptable, with a service and a policy Everything together: @ScriptVariable needing the request, @OSGiService for syllabus point 10, and a hard ceiling.
package com.energy.core.models;
import com.day.cq.wcm.api.Page; import com.day.cq.wcm.api.PageManager; import com.day.cq.wcm.api.designer.Style; import com.energy.core.services.ProductDataService; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.ScriptVariable; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List;
@Model( // REQUEST-adaptable, because @ScriptVariable needs the HTL // bindings, which only exist during a render. adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class CategoryListingModel {
}Copy Copy What to point at when walking an interviewer through this:
Why request-adaptable — @ScriptVariable for currentPage, currentStyle and pageManager. Without those three, this model would adapt from Resource and be easier to test.
@OSGiService not @Reference — with the reason, not just the fact.
A hard ceiling constant — an author typing a bad root path should produce an empty list, not a repository walk that takes the instance down.
Every adaptTo is null-checked, including inside the loop.
The limit falls back through three sources — author, then policy, then constant. That policy fallback is file 03 meeting file 05.
8.4 The interface pattern — how Core Components do it // Exported package: the public contract package com.energy.core.models;
public interface Cta { String getLinkText(); String getLinkUrl(); String getStyle(); boolean isReady(); }Copy Copy // NON-exported .impl package: the implementation package com.energy.core.models.impl;
import com.energy.core.models.Cta; // ...
@Model( adaptables = Resource.class, adapters = Cta.class, // register AS the interface resourceType = CtaImpl.RESOURCE_TYPE, // bind to a resource type defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class CtaImpl implements Cta {
}Copy Copy HTL uses the interface:
8.5 Unit test — testing what actually breaks package com.energy.core.models;
import com.energy.core.services.ProductDataService; import io.wcm.testing.mock.aem.junit5.AemContext; import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.apache.sling.api.resource.Resource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(AemContextExtension.class) class FaqModelTest {
}Copy Copy Two things to call out. You must register both model classes, or @ChildResource cannot adapt the children — that is a real and confusing test failure. And the tests target the cases authors actually create: no rows, a half-filled row, two instances on a page. Testing that a getter returns its field proves nothing.
- Common Mistakes The mistake What actually happens The fix Package not registered for scanning Model silently returns null Sling-Model-Packages; check status-adapters Wrong adaptables Silent null when adapting from the other type Declare what you actually adapt from Leaving defaultInjectionStrategy as REQUIRED One blank field makes the whole component vanish OPTIONAL for component models Using @Inject instead of injector-specific Ambiguous, slower, can pick up a script binding @ValueMapValue, @ChildResource, etc. Doing work in a getter HTL calls it repeatedly — sometimes hundreds of times @PostConstruct, store in a field Trying to initialise in the constructor Fields are still null there @PostConstruct Returning null from a getter HTL renders "null", or you need guards everywhere Empty string / empty list Not null-checking adaptTo NullPointerException, the classic AEM one Always guard, including inside loops @Reference in a model Does not compile meaningfully / never injects @OSGiService int instead of Integer Missing property becomes 0, indistinguishable from a real 0 Use the wrapper type boolean where "not set" matters Missing property becomes false Boolean @ValueMapValue for a child node Nothing injected — it only reads properties @ChildResource Forgetting to register the nested model in tests @ChildResource list is empty, confusing failure addModelsForClasses(Parent.class, Child.class) Request-adaptable when Resource would do Harder to test, unusable outside a render Default to Resource.class Unbounded list building A bad path walks the whole repository A hard ceiling constant Blocking external call in the model Slow pages, threads hang without a timeout OSGi service, timeouts, cache, or fetch client-side Property name mismatch with no @Named Silent null under OPTIONAL @Named("jcr:title")
- Best Practices On adaptables. Default to Resource.class. Move to the request only when you genuinely need @ScriptVariable or request data — and notice when that is only one or two fields, because it may mean the model should be split.
On annotations. Always injector-specific. @Inject is legacy: it works, but it is ambiguous and occasionally picks up the wrong source.
On @PostConstruct. Derive there, build lists there, do lookups there. Keep getters trivial. Assume every getter can be called many times.
On null. Never return null from a getter — empty string for text, empty list for collections. Always null-check adaptTo, including inside loops. Use wrapper types where "not set" and "zero/false" mean different things.
On structure. One model per component, and split when a model grows past roughly a couple of hundred lines or starts serving several unrelated concerns. Interface plus non-exported .impl for anything reused or exported.
On safety. A hard ceiling on anything that builds a collection. Timeouts on anything that leaves the JVM. Log at warn when a configured path does not resolve, so a misconfiguration is visible rather than silent.
On testing. Test the empty case, the partial case and the two-instances-on-a-page case. Register nested models. Mock injected services with context.registerService.
- Debugging Tips The single most useful console: /system/console/status-adapters. If your model's adapter is not listed there, the class is not registered at all — a scanning problem, not a code problem. Checking this first saves a lot of time staring at annotations.
The five-cause checklist for a null model, in order:
Package not scanned → status-adapters Wrong adaptable → does adaptables match what you called adaptTo on? A REQUIRED injection failed → switch to OPTIONAL temporarily to see if it appears @PostConstruct threw → error.log Resource type mismatch → only if the model is resourceType-bound A quick test from HTL. Output the model object itself:
${listing}Copy Copy An object reference means the model resolved and the problem is in a getter. Nothing at all means the adaptation returned null.
When a value is missing but the model resolves: the property name and the field name probably do not match, and OPTIONAL is hiding it. Open the node in CRXDE and compare the actual property name against your field. This is the same "split the problem in half" move from file 02 — is the data absent, or is the reading wrong?
When something is slow: add a temporary log line at the top of the suspect getter and count how often it appears for one page render. If it is more than once, the work belongs in @PostConstruct.
The Sling Log Tracer at /system/console/tracer lets you trace what happened during a single request without redeploying — useful when the problem only reproduces on a specific page.
Tool What it tells you /system/console/status-adapters Whether the model is registered at all /system/console/tracer What happened during one request /system/console/bundles Whether the core bundle is even ACTIVE error.log @PostConstruct exceptions ${model} in HTL Did the adaptation succeed /crx/de The real property names on the node 12. Performance Optimization Expensive work goes in @PostConstruct. This is the biggest single lever, and story 1 shows why — a getter used three times in HTL, over a twenty-item list, ran two hundred times.
Prefer Resource adaptables. Cheaper to create and testable without a request.
Injector-specific annotations. @Inject may try several injectors per field. On a page with fifty component instances that adds up.
A hard ceiling on collections. Not just for correctness — an unbounded listChildren() under a misconfigured root can genuinely take an instance down.
Prefer a bounded traversal over a repository-wide query. Walking children of a known path needs no Oak index and has predictable cost. If you must query, it must be indexed and limited.
Never block on an external call during rendering without a timeout, and think hard about whether that data should be fetched client-side instead so the page stays dispatcher-cacheable.
cache = true when the same model is adapted from several places in one render and does real work. Not worth it for a model reading three properties.
2 sections hidden in Learn mode 13. Real Production Scenarios · 14. Follow-up Questions Show 15. Comparison Tables Resource.class vs SlingHttpServletRequest.class
Resource.class SlingHttpServletRequest.class Gives access to Content only Content plus request context @ValueMapValue Yes Yes (via the request's resource) @ChildResource Yes Yes @ScriptVariable No Yes Selectors, suffix, parameters No Yes Unit testing Easier More setup Usable from a scheduler or servlet Yes No Use as The default Only when you need request data @Inject vs injector-specific
@Inject @ValueMapValue etc. Source Tries every injector in ranking order One specific injector Self-documenting No Yes Performance Slower Faster Risk Can pick up a script binding None Status Legacy but functional Preferred @ValueMapValue vs @ChildResource
@ValueMapValue @ChildResource Reads A property on this node A child node Types String, Boolean, Integer, arrays, Calendar Resource, a model, or a List of models Use for Dialog fields Composite multifields, nested structures @OSGiService vs @Reference — syllabus points 10 and 11
@OSGiService @Reference Used inside A Sling Model An OSGi @Component Injected by Sling Models framework OSGi Declarative Services Works in a model Yes No Works in a servlet or service No Yes Filtering filter = "(...)" target = "(...)" REQUIRED vs OPTIONAL
REQUIRED (default) OPTIONAL Missing value Whole model → null That field → null Good for Genuinely mandatory config Component models Downside One blank field kills the component Hides typos Sling Model vs WCMUsePojo vs scriptlet
Scriptlet WCMUsePojo Sling Model Where the logic lives In the view A class extending an AEM base class A plain annotated class Unit testable No Awkward Yes Base class required — Yes No Model Exporter / headless No No Yes Status Dead Legacy Current