> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

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

1. 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 \{

```
@ValueMapValue
private String title;

@ValueMapValue
private String description;

public String getTitle() { return title; }
public String getDescription() { return description; }
```

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

2. 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:

<sly data-sly-use.cta="com.energy.core.models.CtaModel"/>
${cta.linkText}Copy
Copy
Or from Java:

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;

```
public Normal(String name) {
    this.greeting = "Hello " + name;   // constructor does the setup
}
```

}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<br/>the instance<br/><br/>CONSTRUCTOR RUNS<br/>fields are still null"] --> B\["2. Sling INJECTS<br/>values into fields<br/><br/>now the fields<br/>have values"]
B --> C\["3. Sling calls the<br/>@PostConstruct method<br/><br/>safe to use<br/>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 \{

```
@ValueMapValue
private String linkUrl;

private boolean external;

public CtaModel() {
    // linkUrl is NULL here. Injection hasn't happened yet.
}

@PostConstruct
protected void init() {
    // linkUrl HAS its value now. Safe.
    this.external = linkUrl != null && linkUrl.startsWith("http");
}
```

}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<CardModel> getCards() \{
return buildCardList();     // expensive
}

// GOOD -- runs exactly once
private List<CardModel> cards;

@PostConstruct
protected void init() \{
this.cards = buildCardList();
}

public List<CardModel> getCards() \{
return cards;               // just returns the field
}Copy
Copy
Use 3 — validate, and make the model refuse to exist.

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 \{

```
@OSGiService
private ProductDataService productDataService;

@PostConstruct
protected void init() {
    this.specs = productDataService.getSpecifications(productId);
}
```

}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   = "<p>It changes the voltage level...</p>"
└── item1/
├── question = "What do electrical transformers do?"
└── answer   = "<p>They transfer energy between circuits...</p>"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<FaqItemModel> faqs;   // faqs/item0, faqs/item1, ... each adaptedCopy
Copy
That last form is why the annotation matters in practice. A composite multifield produces child nodes, and @ChildResource with a List is how you read them. Section 8 has the full working example.

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<FaqItemModel> with @ChildResource and Sling adapts each child node into that nested model class.

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:

<sly data-sly-use.cta="com.energy.core.models.Cta"/>Copy
Copy
Why bother? Three reasons:

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.

3. 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?<br/>/system/console/status-adapters"}
B -->|No| C\["1. PACKAGE NOT SCANNED<br/>Sling-Model-Packages header"]
B -->|Yes| D\{"Does adaptables match<br/>what you adapted from?"}
D -->|No| E\["2. WRONG ADAPTABLE<br/>e.g. adapting a request to a<br/>Resource-only model"]
D -->|Yes| F\{"Any REQUIRED injection<br/>that found nothing?"}
F -->|Yes| G\["3. REQUIRED INJECTION FAILED<br/>use OPTIONAL, or fix the property"]
F -->|No| H\{"Did @PostConstruct throw?"}
H -->|Yes| I\["4. POSTCONSTRUCT THREW<br/>check error.log"]
H -->|No| J\["5. RESOURCE TYPE MISMATCH<br/>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:

1. script-bindings      ← currentPage, currentStyle, wcmmode, pageManager
2. value-map            ← node properties
3. child-resources      ← child nodes
4. request-attributes
5. osgi-services
6. resource-path
7. self
8. 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,

```
    // Authors leave fields blank constantly. With REQUIRED (the
    // default), one empty optional field would make the whole
    // model null and the component would vanish.
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
```

)
public class CtaModel \{

```
// Injector-SPECIFIC. Goes straight to the ValueMap.
// Field name = property name.
@ValueMapValue
private String linkText;

@ValueMapValue
private String linkUrl;

@ValueMapValue
private String style;

// Boolean (not boolean): a missing property gives null, which we
// can distinguish from an author explicitly setting false.
// This is the uncheckedValue situation from file 02.
@ValueMapValue
private Boolean newTab;

// @SlingObject gets us common Sling objects without a request
@SlingObject
private ResourceResolver resourceResolver;

// Derived fields -- NOT injected, computed in @PostConstruct
private String resolvedUrl;
private boolean openInNewTab;

/**
 * Runs AFTER injection. In the constructor every field above
 * would still be null, which is exactly why this hook exists.
 */
@PostConstruct
protected void init() {
    this.resolvedUrl  = resolveUrl(linkUrl);
    this.openInNewTab = Boolean.TRUE.equals(newTab) || isDocument(linkUrl);
}

private String resolveUrl(String url) {
    if (StringUtils.isBlank(url)) {
        return null;
    }
    if (url.startsWith("/content")) {
        String mapped = resourceResolver.map(url);
        return url.startsWith("/content/dam") ? mapped : mapped + ".html";
    }
    return url;
}

private boolean isDocument(String url) {
    return url != null && StringUtils.endsWithAny(
            url.toLowerCase(), ".pdf", ".docx", ".xlsx");
}

// GETTERS -- HTL can only reach values through these.
// ${cta.linkText} calls getLinkText().

public String getLinkText() {
    return StringUtils.defaultString(linkText);   // never null
}

public String getLinkUrl() {
    return resolvedUrl;
}

public String getStyle() {
    return StringUtils.defaultIfBlank(style, "arrow");
}

/** Returning null means HTL omits the attribute entirely. */
public String getTarget() {
    return openInNewTab ? "_blank" : null;
}

public String getRel() {
    return openInNewTab ? "noopener noreferrer" : null;
}

public boolean isReady() {
    return StringUtils.isNotBlank(linkText) && StringUtils.isNotBlank(resolvedUrl);
}
```

}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 \{

```
// These read properties on the CHILD node (faqs/item0),
// because that is the resource this model was adapted from.
@ValueMapValue
private String question;

@ValueMapValue
private String answer;

// The child node's OWN resource -- used for a stable unique id
@SlingObject
private Resource resource;

public String getQuestion() {
    return StringUtils.defaultString(question);
}

public String getAnswer() {
    return StringUtils.defaultString(answer);
}

/**
 * IDs derived from the resource PATH, not a counter.
 * A counter restarts at 0 for each accordion, so two accordions
 * on one page would both emit id="faq-0" -- duplicate IDs break
 * the aria-controls relationships and are invalid HTML.
 */
public String getId() {
    return "faq-" + Math.abs(resource.getPath().hashCode());
}

public boolean isValid() {
    return StringUtils.isNotBlank(question) && StringUtils.isNotBlank(answer);
}
```

}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 \{

```
// A PROPERTY on this node
@ValueMapValue
private String heading;

/**
 * CHILD NODES of this node.
 *
 * A composite multifield stores each row as a child node:
 *     faqs/item0/question, faqs/item0/answer
 *     faqs/item1/question, faqs/item1/answer
 *
 * @ValueMapValue could never see these -- they are not properties
 * on this node. @ChildResource reads them, and Sling adapts each
 * child into FaqItemModel because that is the List's type.
 */
@ChildResource
private List<FaqItemModel> faqs;

private List<FaqItemModel> validFaqs;

@PostConstruct
protected void init() {
    // Filter ONCE here, not in the getter -- HTL may call
    // getFaqs() several times per render.
    this.validFaqs = (faqs == null)
            ? Collections.emptyList()
            : faqs.stream()
                  .filter(FaqItemModel::isValid)
                  .collect(Collectors.toList());
}

public String getHeading() {
    return StringUtils.defaultString(heading);
}

/** Empty list, never null -- data-sly-list handles empty cleanly. */
public List<FaqItemModel> getFaqs() {
    return validFaqs;
}

public boolean isReady() {
    return !validFaqs.isEmpty();
}
```

}Copy
Copy
The three points to make about this pair:

@ChildResource on a List<FaqItemModel> reads the child nodes and adapts each one into the nested model. Sling works out the target type from the generic parameter.

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 \{

```
private static final Logger LOG =
        LoggerFactory.getLogger(CategoryListingModel.class);

/** Hard ceiling. A misconfigured root should degrade, not kill the instance. */
private static final int MAX_ITEMS = 200;

// --- content: dialog properties ---
@ValueMapValue
private String rootPath;

@ValueMapValue
private Integer maxItems;

// --- request-only: HTL bindings ---

/** The page being rendered. Needs the request. */
@ScriptVariable
private Page currentPage;

/** The RESOLVED POLICY for this component on this template -- file 03. */
@ScriptVariable
private Style currentStyle;

@ScriptVariable
private PageManager pageManager;

// --- an OSGi service: SYLLABUS POINT 10 ---

/**
 * @OSGiService, NOT @Reference.
 *
 * @Reference is an OSGi Declarative Services annotation and only
 * works in classes DS manages -- i.e. @Component classes. A Sling
 * Model is created by the Sling Models framework, so DS never
 * sees it and cannot inject into it.
 */
@OSGiService
private ProductDataService productDataService;

private List<CardModel> cards;
private String effectiveRootPath;

@PostConstruct
protected void init() {
    // Fall back to the current page if the author left it blank
    this.effectiveRootPath = StringUtils.isNotBlank(rootPath)
            ? rootPath
            : currentPage.getPath();

    this.cards = collectCards();
}

private List<CardModel> collectCards() {
    Page root = pageManager.getPage(effectiveRootPath);
    if (root == null) {
        LOG.warn("Listing root not found: {}", effectiveRootPath);
        return Collections.emptyList();
    }

    // A bounded traversal under a KNOWN root needs no Oak index
    // and has predictable cost. A repo-wide query would need one.
    int limit = resolveLimit();
    List<CardModel> result = new ArrayList<>();
    Iterator<Page> children = root.listChildren();

    while (children.hasNext() && result.size() < limit) {
        Page child = children.next();
        if (child.isHideInNav()) {
            continue;
        }
        // adaptTo can ALWAYS return null -- guard it
        CardModel card = child.getContentResource() != null
                ? child.getContentResource().adaptTo(CardModel.class)
                : null;
        if (card != null && card.isValid()) {
            result.add(card);
        }
    }
    return result;
}

private int resolveLimit() {
    // Author value, else the POLICY default, else our constant
    if (maxItems != null && maxItems > 0) {
        return Math.min(maxItems, MAX_ITEMS);
    }
    int fromPolicy = currentStyle != null
            ? currentStyle.get("defaultMaxItems", 12)
            : 12;
    return Math.min(fromPolicy, MAX_ITEMS);
}

public List<CardModel> getCards() {
    return cards;
}

public boolean isReady() {
    return !cards.isEmpty();
}
```

}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 \{

```
protected static final String RESOURCE_TYPE = "energy/components/cta";

// ... same fields and logic as 8.1
```

}Copy
Copy
HTL uses the interface:

<sly data-sly-use.cta="com.energy.core.models.Cta"/>Copy
Copy
Three benefits, worth stating: the implementation can change without touching HTL; the resourceType binding lets Sling pick the right implementation and is required by the Model Exporter; and the impl package stays unexported, which is proper OSGi module hygiene from file 01.

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 \{

```
private final AemContext context = new AemContext();

@BeforeEach
void setUp() {
    context.load().json("/faq/content.json", "/content");

    // Register BOTH the parent and the nested model, or
    // @ChildResource cannot adapt the child nodes.
    context.addModelsForClasses(FaqModel.class, FaqItemModel.class);

    // Mock any injected OSGi service
    context.registerService(ProductDataService.class,
            Mockito.mock(ProductDataService.class));
}

@Test
void readsQuestionsFromChildNodes() {
    FaqModel model = adapt("/content/faq");
    assertEquals(2, model.getFaqs().size());
    assertEquals("What is a transformer in electricity?",
            model.getFaqs().get(0).getQuestion());
}

@Test
void returnsEmptyListWhenAuthorAddedNoRows() {
    // The injected List is NULL here, not empty.
    // This is the case @PostConstruct normalises.
    FaqModel model = adapt("/content/faq-empty");
    assertNotNull(model);
    assertTrue(model.getFaqs().isEmpty(), "must be empty, never null");
    assertFalse(model.isReady());
}

@Test
void skipsRowsWhereTheAnswerIsBlank() {
    // Authors WILL do this. Most common real-world case.
    FaqModel model = adapt("/content/faq-partial");
    assertEquals(1, model.getFaqs().size());
}

@Test
void twoAccordionsOnOnePageProduceDifferentIds() {
    String first  = adapt("/content/page/faq-one").getFaqs().get(0).getId();
    String second = adapt("/content/page/faq-two").getFaqs().get(0).getId();
    assertNotEquals(first, second, "duplicate IDs break aria-controls");
}

private FaqModel adapt(String path) {
    Resource resource = context.resourceResolver().getResource(path);
    assertNotNull(resource, "test content missing at " + path);
    FaqModel model = resource.adaptTo(FaqModel.class);
    assertNotNull(model, "model failed to adapt at " + path);
    return model;
}
```

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

9. 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")
10. 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.

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