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

# OSGi and Service

Before we start — don't memorise the list
Your syllabus says "memorise all the OSGi annotations." That is what the candidate wrote down, and it is understandable — but it is the wrong way to prepare, and it will show in an interview.

There are roughly fifteen annotations that matter. Memorised as a flat list, they blur together and you will fumble under pressure. But they fall into five groups by purpose, and each group answers one question:

Group	The question it answers

1. Declaring	How do I tell OSGi this class exists?
2. Lifecycle	How do I run code when it starts and stops?
3. Injection	How do I get hold of another service?
4. Configuration	How do I make settings change per environment?
5. Registration	How do I register a servlet or a filter?
   Learn the groups, and you can derive the annotation you need rather than recalling it. That is also how you sound in an interview — someone who understands the model, not someone reciting.

There is also a natural continuity from file 05. That file ended on: "@Reference doesn't work in a Sling Model because a model isn't an OSGi component." This file is the other half of that sentence — what an OSGi component actually is, and why @Reference works there.

1. Introduction
   1.1 A quick recap of why OSGi exists
   From file 01, but worth restating because everything here builds on it.

The problem with plain Java. Build a normal Java web application and you get one big classpath. Every JAR can see every other JAR. There is no way to say "this package is internal, don't use it." And changing one class means redeploying and restarting everything.

OSGi's answer is the bundle — a JAR with extra manifest information declaring two things:

Export-Package — "other bundles may use these packages of mine"
Import-Package — "I need these packages from someone else"
Because those dependencies are explicit, OSGi knows exactly what depends on what. Which means it can stop one bundle, replace it, and restart it without touching anything else.

The apartment building analogy from file 01: each flat is a bundle, with its own front door and plumbing. You can renovate flat 3B without evacuating the building, because the building knows what 3B connects to.

1.2 The bit that file 01 left out — how bundles talk to each other
Here is the interesting problem. Bundles are deliberately isolated. So how does code in bundle A actually use something from bundle B?

Not by new-ing it. Bundle A does not have bundle B's implementation class on its classpath — and even if it did, hardcoding new ProductDataServiceImpl() would defeat the whole point of modularity.

OSGi's answer is the service registry.

Think of it as a noticeboard in the middle of the building:

Bundle B publishes a notice: "I provide ProductDataService. Here's my instance."
Bundle A looks at the board: "Does anyone provide ProductDataService?"
OSGi hands A the instance B published.
Bundle A never knows which class implements it. It only knows the interface. Swap the implementation tomorrow and A does not change or even notice.

That noticeboard is the single most important concept in this file. Every question about services, @Reference, and "why is my component unsatisfied" comes back to it.

1.3 Declarative Services — who does the work
Publishing to and reading from that registry by hand is verbose. So OSGi has a layer called Declarative Services, usually shortened to DS.

DS is what makes annotations work. You declare what you provide and what you need, and the DS runtime does the actual registering and wiring.

graph LR
A\["Your class<br/>@Component + @Reference"] -->|"DS reads<br/>the annotations"| B\["DS runtime"]
B -->|"creates the instance"| C\["Your object"]
B -->|"publishes to"| D\["Service registry<br/>(the noticeboard)"]
B -->|"looks up and injects"| C
⛶ Fullscreen
Remember this, because it is the answer to a syllabus question: the DS runtime only manages classes annotated with @Component. That is precisely why @Reference cannot work in a Sling Model — DS never sees it.

1.4 A real project example to adapt
"On our project the core bundle has around fifteen OSGi services — things like a product data service that talks to the internal product API, a document link resolver for our technical downloads, and a few Sling servlets and filters. Each one is an interface in an exported package with the implementation in a non-exported impl package. Configuration is done with @ObjectClassDefinition and shipped as run-mode-specific JSON files in ui.config, so the API endpoint and timeouts differ between dev, stage and production without any code change."

That covers the interface pattern, configuration, and run modes — three likely follow-ups.

2. Core Concepts
   2.1 Component versus Service (syllabus point 13)
   Your syllabus asks this directly, and it is one of the most common OSGi interview questions. Let's get it exactly right.

A component is a class that the DS runtime manages. You mark it with @Component. DS creates it, calls its lifecycle methods, injects its references, and destroys it. Nothing more.

A service is a component that has additionally been published to the registry under one or more interfaces, so other code can find and use it.

// A COMPONENT. DS manages it, but nobody can look it up.
@Component
public class SomeStartupTask \{
@Activate
protected void activate() \{
// runs at startup, does its work, registers nothing
}
}Copy
Copy
// A COMPONENT _AND_ A SERVICE. Published under ProductDataService.
@Component(service = ProductDataService.class)
public class ProductDataServiceImpl implements ProductDataService \{
// now other bundles can @Reference ProductDataService
}Copy
Copy
The whole difference is the service attribute.

graph TD
A\["@Component<br/>DS manages it, gives it a lifecycle"] --> B\{"Does it have<br/>service = ...?"}
B -->|No| C\["A COMPONENT<br/>runs, but invisible to others"]
B -->|Yes| D\["A COMPONENT _and_ A SERVICE<br/>published on the noticeboard<br/>others can @Reference it"]
⛶ Fullscreen
The relationship, in the sentence interviewers want:

Every service is a component. Not every component is a service.

The interview answer:

"A component is a class the Declarative Services runtime manages — DS creates it, injects its references, and calls its @Activate and @Deactivate methods. If you've used Spring, it's close to a Spring bean.

A service is a component that's additionally been published to the OSGi service registry under an interface, so other code can look it up and use it. You do that with the service attribute on @Component.

So every service is a component, but not every component is a service. A component with no service attribute still has a full lifecycle and still runs — it just isn't discoverable. We use that for things that do work on activation and don't need to be called by anyone else.

The practical significance is the boundary: consumers depend on the interface that's published, never on the implementation class. That's what lets us swap an implementation without touching anything that uses it."

2.2 How to make a class a service (syllabus point 13)
Your syllabus asks this explicitly. Four steps, and step 1 is the one that separates good code from bad.

Step 1 — Define an interface. This is the contract.

// Exported package -- this is public API for other bundles
package com.energy.core.services;

import java.util.List;

public interface ProductDataService \{

```
/**
 * Returns the technical specifications for a product,
 * or an empty list if the product is unknown or the
 * upstream service is unavailable.
 */
List<Specification> getSpecifications(String productId);

boolean isAvailable();
```

}Copy
Copy
Step 2 — Write the implementation in a non-exported impl package.

package com.energy.core.services.impl;   // NOT exported

import com.energy.core.services.ProductDataService;
import org.osgi.service.component.annotations.Component;

@Component(service = ProductDataService.class)
public class ProductDataServiceImpl implements ProductDataService \{
// ...
}Copy
Copy
Step 3 — service = ProductDataService.class is what publishes it. Note it is registered under the interface, not under ProductDataServiceImpl.

Step 4 — Consumers reference the interface:

@Reference
private ProductDataService productDataService;Copy
Copy
Now the question that always follows: why bother with an interface?

Four reasons, and giving several is what makes the answer senior:

One — swappable implementations. We started with a service that called the API directly. Later we added a caching implementation. Because consumers depended on the interface, nothing else changed.

Two — module boundaries. The interface package is exported; the impl package is not. Other bundles physically cannot import the implementation class even if they wanted to. That is the OSGi hygiene point from file 01, enforced rather than merely intended.

Three — testability. Consumers can be unit tested with a mock of the interface.

Four — a documented contract. The interface is where you say what happens when the product is unknown or the upstream is down. That is the kind of thing that gets lost in an implementation.

The interview answer:

"Four steps. Define an interface in an exported package — that's the contract. Write the implementation in a non-exported impl package. Annotate the implementation with @Component(service = MyService.class), which is what actually publishes it to the registry under that interface. Then consumers inject it with @Reference on the interface type.

The interface isn't ceremony. It means we can swap implementations without touching consumers — we did exactly that when we added caching to our product data service. It gives us a real module boundary, since the impl package isn't exported so other bundles physically can't import it. It makes consumers unit testable with a mock. And it's where the contract gets documented — what happens when the upstream is unavailable, for instance."

2.3 The annotations, by purpose (syllabus point 7)
Now the list your syllabus asks for — organised so you can derive rather than recall.

Group 1 — Declaring
Annotation	Purpose
@Component	Tell DS this class exists and should be managed
@Component(service = X.class)	…and publish it as a service under X
Everything in OSGi starts here. There is exactly one declaring annotation, with an attribute that decides component-versus-service.

Group 2 — Lifecycle
Annotation	When it runs
@Activate	When the component starts — dependencies satisfied, config applied
@Modified	When the configuration changes, without a restart
@Deactivate	When the component stops
Group 3 — Injection
Annotation	Purpose
@Reference	Inject another service from the registry
One annotation, but several attributes that change its behaviour substantially — section 2.5.

Group 4 — Configuration
Annotation	Purpose
@ObjectClassDefinition	Define the shape of the configuration
@AttributeDefinition	Define one configuration field
@Designate	Connect that definition to a component
Group 5 — Sling/AEM registration
Annotation	Purpose
@SlingServletResourceTypes	Register a servlet by resource type
@SlingServletPaths	Register a servlet by path
@SlingServletFilter	Register a request filter
These are Sling conveniences. Under the hood they are just @Component with the right service interface and properties — but they are far more readable, and they are the current way. Servlets get their own file (07).

Supporting types you should recognise
Type	Where it's used
ConfigurationPolicy	@Component(configurationPolicy = ...) — OPTIONAL / REQUIRE / IGNORE
ReferenceCardinality	@Reference(cardinality = ...) — MANDATORY / OPTIONAL / MULTIPLE / AT\_LEAST\_ONE
ReferencePolicy	@Reference(policy = ...) — STATIC / DYNAMIC
ReferencePolicyOption	@Reference(policyOption = ...) — RELUCTANT / GREEDY
ServiceScope	@Component(scope = ...) — SINGLETON / BUNDLE / PROTOTYPE
How to answer "list all the OSGi annotations" in an interview:

"I'd group them by what they're for rather than list them flat.

Declaring: @Component, with its service attribute deciding whether it's just a component or also a published service.

Lifecycle: @Activate, @Deactivate, and @Modified — the last one meaning a configuration change doesn't force a full restart of the component.

Injection: @Reference, with cardinality, policy and target controlling whether it's mandatory, whether it can be swapped at runtime, and which implementation to pick.

Configuration: @ObjectClassDefinition and @AttributeDefinition to define the settings, and @Designate to attach them to a component.

Sling registration: @SlingServletResourceTypes, @SlingServletPaths and @SlingServletFilter, which are readable wrappers over @Component for servlets and filters.

And I'd add that the older Felix SCR annotations — @Service and @Property as separate annotations — are deprecated. In current code @Service is replaced by the service attribute and @Property by @ObjectClassDefinition."

That last paragraph is worth including. It shows you can read an older codebase and know what to modernise.

2.4 @Component — the attributes that matter
@Component(
service = ProductDataService.class,
immediate = true,
configurationPolicy = ConfigurationPolicy.REQUIRE,
property = \{ "service.ranking:Integer=100" },
scope = ServiceScope.SINGLETON
)Copy
Copy
service — which interface(s) to publish under. Omit it and you have a component that is not a service. You can list several: service = \{ProductDataService.class, Runnable.class}.

immediate — this one has non-obvious default behaviour, so it comes up.

By default, a component that provides a service is delayed — DS does not create the instance until somebody actually asks for the service. That is a sensible optimisation.

But sometimes you need the component to start doing something whether or not anyone consumes it — a listener, a warm-up task, something that registers with an external system. immediate = true forces activation as soon as its dependencies are satisfied.

configurationPolicy — three values:

Value	Behaviour
OPTIONAL (default)	Activates with or without configuration
REQUIRE	Only activates if a configuration exists
IGNORE	Ignores configuration entirely
REQUIRE is genuinely useful. If a service is meaningless without an API endpoint, REQUIRE means it stays inactive rather than activating in a broken state. It is also how factory configurations work — one component instance per configuration.

But it is also a classic support ticket: "my component never activates" when the answer is configurationPolicy = REQUIRE and the config file was never deployed.

property — service properties, used for filtering and ranking. The :Integer syntax types the value:

property = \{ "service.ranking:Integer=100" }Copy
Copy
scope — how many instances exist:

Value	Behaviour
SINGLETON (default)	One instance shared by everyone
BUNDLE	One instance per consuming bundle
PROTOTYPE	A new instance per consumer
SINGLETON has a consequence people miss, and it is a strong point to raise unprompted: one instance is shared across every request and every thread. So any mutable instance field is shared state. Section 2.8.

2.5 @Reference — calling a service (syllabus point 11)
Your syllabus asks: "which annotation do you use when calling a service class from a servlet or another service?"

The answer is @Reference.

@Component(service = Servlet.class)
public class ProductServlet extends SlingSafeMethodsServlet \{

```
@Reference
private ProductDataService productDataService;
```

}Copy
Copy
That is field injection, and it is what you will write almost every time.

And immediately, the comparison your syllabus is really driving at — points 10 and 11 side by side:

In a Sling Model	In an OSGi component
Annotation	@OSGiService	@Reference
Injected by	Sling Models framework	OSGi Declarative Services
Class is marked	@Model	@Component
Examples	Component models	Services, servlets, filters, schedulers, listeners
Why they are different, in one sentence: "DS only manages @Component classes, and a Sling Model isn't one — it's created by the Sling Models framework, so DS never sees it and can't inject into it."

Now the @Reference attributes that change behaviour.

cardinality — how many, and is it mandatory?

// MANDATORY (1..1) -- the default.
// The component will NOT activate without it.
@Reference
private ProductDataService productDataService;

// OPTIONAL (0..1) -- activates even if absent. Must be volatile.
@Reference(cardinality = ReferenceCardinality.OPTIONAL,
policy = ReferencePolicy.DYNAMIC)
private volatile AnalyticsService analyticsService;

// MULTIPLE (0..n) -- a list, possibly empty
@Reference(cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
private volatile List<ContentEnricher> enrichers;Copy
Copy
Cardinality	Meaning
MANDATORY (default)	Exactly one. No service → component never activates.
OPTIONAL	Zero or one. Activates regardless.
MULTIPLE	Zero or more, injected as a List.
AT\_LEAST\_ONE	One or more.
MANDATORY being the default is the single most important thing here, because it is the direct cause of the most common OSGi production problem: a component sitting unsatisfied and never activating because one service it needs is not there. Section 3.3.

policy — can the reference change while running?

Policy	Behaviour
STATIC (default)	If the service goes away or is replaced, the component is deactivated and reactivated
DYNAMIC	The reference is swapped in place; the component keeps running
DYNAMIC requires the field to be volatile, because another thread will write it while your threads read it.

policyOption — should it switch to a better service?

Option	Behaviour
RELUCTANT (default)	Once bound, stay bound even if a higher-ranked service appears
GREEDY	Rebind to a higher-ranked service when one shows up
target — which implementation, when there are several?

@Reference(target = "(component.name=com.energy.core.services.impl.CachedProductDataService)")
private ProductDataService productDataService;Copy
Copy
That is an LDAP filter over the service's properties.

The interview answer for point 11:

"@Reference. In a servlet or another service, I annotate a field with the service interface type and DS injects it.

The attribute I actually think about is cardinality. It defaults to MANDATORY, which means the component won't activate at all if that service isn't available — and that's the most common OSGi problem in production. A component sits 'unsatisfied' in the components console and you get no error at runtime, just a servlet that never responds. So for genuinely optional dependencies I set OPTIONAL with a DYNAMIC policy and a volatile field, so the component still activates and degrades gracefully.

The distinction I'd make explicit: @Reference only works inside an OSGi component, because DS is what does the injecting. In a Sling Model you use @OSGiService instead, since a model isn't managed by DS."

2.6 Lifecycle — @Activate, @Deactivate, @Modified
@Component(service = ProductDataService.class)
@Designate(ocd = ProductDataServiceConfig.class)
public class ProductDataServiceImpl implements ProductDataService \{

```
private String apiEndpoint;
private CloseableHttpClient httpClient;

@Activate
@Modified
protected void activate(ProductDataServiceConfig config) {
    this.apiEndpoint = config.apiEndpoint();
    this.httpClient  = buildClient(config);
}

@Deactivate
protected void deactivate() {
    // Release anything you hold. Not doing this is a leak.
    IOUtils.closeQuietly(httpClient);
}
```

}Copy
Copy
@Activate runs when the component starts — all mandatory references satisfied, configuration applied. It is the right place to read configuration and set up resources.

@Deactivate runs when it stops — a bundle stop, a redeploy, or a configuration change under STATIC policy. This is where you release things: close HTTP clients, shut down thread pools, unregister listeners. Skipping it is a genuine leak, and it bites on every redeploy since components stop and start constantly during development.

@Modified is the interesting one, and a good interview point.

Without @Modified, changing a configuration causes DS to deactivate and reactivate the component — a full teardown and rebuild.

With @Modified, DS just calls that method with the new configuration. The component keeps running and keeps its state.

Why it matters: if your @Activate builds an HTTP connection pool or opens a connection, a config change without @Modified tears all of that down and rebuilds it. Notice above that @Activate and @Modified point at the same method — that is the common pattern, because usually you want identical behaviour either way.

@Activate signatures. DS accepts several, and picks based on the parameter type:

@Activate protected void activate() \{ }
@Activate protected void activate(MyConfig config) \{ }              // typed config -- preferred
@Activate protected void activate(Map\<String, Object> props) \{ }    // raw properties
@Activate protected void activate(ComponentContext ctx) \{ }         // full context
@Activate protected void activate(BundleContext ctx) \{ }Copy
Copy
The typed-config form is the modern, readable one.

2.7 Configuration — @ObjectClassDefinition and @Designate
This is how a service gets settings that differ per environment.

Step 1 — Define the configuration shape.

Note it is an @interface — a Java annotation type, not a class or an interface. That surprises people the first time.

package com.energy.core.services.impl;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(
name = "Energy — Product Data Service",
description = "Connection settings for the internal product API"
)
public @interface ProductDataServiceConfig \{

```
@AttributeDefinition(
        name = "API Endpoint",
        description = "Base URL of the product API",
        type = AttributeType.STRING
)
String apiEndpoint() default "https://api.internal/v1";

@AttributeDefinition(
        name = "Connect Timeout (ms)",
        description = "Give up if the connection is not established in this time"
)
int connectTimeout() default 3000;

@AttributeDefinition(
        name = "Socket Timeout (ms)",
        description = "Give up if no data arrives in this time"
)
int socketTimeout() default 5000;

@AttributeDefinition(name = "Cache TTL (seconds)")
int cacheTtl() default 300;

@AttributeDefinition(
        name = "Enabled",
        description = "Turn the integration off without redeploying"
)
boolean enabled() default true;
```

}Copy
Copy
Each method becomes a configuration property. The method name is the property name; default supplies the fallback.

Step 2 — Attach it with @Designate.

@Component(service = ProductDataService.class)
@Designate(ocd = ProductDataServiceConfig.class)
public class ProductDataServiceImpl implements ProductDataService \{

```
@Activate
@Modified
protected void activate(ProductDataServiceConfig config) {
    // typed access -- no string keys, no casting
    this.apiEndpoint = config.apiEndpoint();
    this.timeout     = config.connectTimeout();
}
```

}Copy
Copy
Step 3 — Ship configuration values as JSON, per run mode.

ui.config/.../apps/energy/osgiconfig/config.publish.prod/com.energy.core.services.impl.ProductDataServiceImpl.cfg.json

\{
"apiEndpoint": "[https://api.internal.company.com/v2](https://api.internal.company.com/v2)",
"connectTimeout": 3000,
"socketTimeout": 5000,
"cacheTtl": 600,
"enabled": true
}Copy
Copy
Two naming rules that cause real problems when broken:

The filename must be the component's PID, which by default is the fully-qualified class name. A typo means the config file exists, deploys successfully, and is silently never applied. There is no error.

The folder name selects the run mode — config.publish.prod from file 01. That is how the endpoint differs between environments with one codebase.

Factory configurations — when you need several instances of the same component with different settings:

@Designate(ocd = MyConfig.class, factory = true)Copy
Copy
Then the filenames carry an instance suffix after a tilde:

com.energy.core.services.impl.MyServiceImpl~~products.cfg.json
com.energy.core.services.impl.MyServiceImpl~~downloads.cfg.jsonCopy
Copy
Each file produces its own component instance with its own configuration.

2.8 Thread safety — the point most candidates miss
This is worth knowing because raising it unprompted marks you out.

A SINGLETON service — the default — is one instance shared across every request and every thread.

On a publish instance under load, that means dozens of threads inside your service simultaneously. So:

@Component(service = ProductDataService.class)
public class BadServiceImpl implements ProductDataService \{

```
// SHARED MUTABLE STATE. Every request thread writes this.
private String lastProductId;          // BUG

@Override
public List<Specification> getSpecifications(String productId) {
    this.lastProductId = productId;    // race condition
    return fetch(this.lastProductId);  // may be another thread's value
}
```

}Copy
Copy
Two requests arrive at once, both write lastProductId, and one of them reads the other's value. It works perfectly in testing and fails intermittently under load — the worst kind of bug.

The rule: in a singleton service, instance fields must be immutable after activation (configuration read once in @Activate is fine) or genuinely thread-safe. Anything request-specific belongs in a local variable or a parameter.

Say this in an interview and it lands:

"One thing I'm careful about is that a singleton service — which is the default scope — is shared across every request thread on the instance. So instance fields have to be either immutable after @Activate or thread-safe. Configuration read once in @Activate is fine because it doesn't change per request, but anything request-specific has to be a local variable. Mutable instance state is the classic bug there: it passes every test and then fails intermittently under production load."

2.9 Old versus new annotations
You will meet older code, and knowing the difference is a small credibility marker.

Old (Felix SCR — deprecated)	Current (OSGi DS R6/R7)
@Component from org.apache.felix.scr.annotations	@Component from org.osgi.service.component.annotations
@Service — a separate annotation	The service attribute on @Component
@Property on the class	@ObjectClassDefinition + @AttributeDefinition
@Reference from Felix	@Reference from org.osgi.service.component.annotations
@Activate with a Map	@Activate with a typed config
The one to remember: @Service no longer exists as an annotation. If you see @Service in a codebase, that is the old Felix SCR style, and the modern equivalent is service = X.class on @Component.

This also explains an interview question that otherwise seems like a trick: "what does @Service do?" The right answer is that it is the deprecated Felix annotation, replaced by the service attribute.

3. Internal Working
   3.1 The component lifecycle
   stateDiagram-v2
   \[_] --> Disabled
   Disabled --> Unsatisfied: bundle started, component enabled
   Unsatisfied --> Satisfied: all MANDATORY references available<br/>+ config present (if REQUIRE)
   Satisfied --> Active: instance created, @Activate called
   Active --> Active: config changed + @Modified present
   Active --> Satisfied: @Deactivate called
   Satisfied --> Unsatisfied: a mandatory reference disappeared
   Unsatisfied --> \[_]: bundle stopped
   ⛶ Fullscreen
   The state that matters in practice is Unsatisfied. It means DS knows about your component but will not create it, because something it declared as mandatory is missing.

Two things put a component there:

A MANDATORY @Reference whose service is not registered.
configurationPolicy = REQUIRE with no configuration present.
Both produce the same symptom: your code simply never runs, with no exception anywhere.

3.2 What happens when a service is published and consumed
sequenceDiagram
participant B as Bundle starts
participant DS as DS runtime
participant R as Service registry
participant C as Consumer component

```
B->>DS: bundle ACTIVE, components discovered
DS->>DS: read @Component annotations
DS->>DS: check mandatory references + config
DS->>DS: create instance
DS->>DS: inject @Reference fields
DS->>DS: call @Activate
DS->>R: publish under the declared interface
R-->>DS: registered

C->>DS: I have @Reference ProductDataService
DS->>R: who provides ProductDataService?
R-->>DS: this instance
DS->>C: inject it
DS->>C: call @Activate on the consumer
```

⛶ Fullscreen
Notice the ordering: references are injected before @Activate. That is why it is safe to use an injected service inside @Activate — the same reasoning as @PostConstruct in file 05, and worth pointing out as a pattern AEM applies consistently.

3.3 The unsatisfied-component problem — the top production question
"My servlet isn't responding" or "my service isn't doing anything" with no errors in the log. Here is the diagnosis.

graph TD
A\["My code never runs,<br/>no errors in the log"] --> B\{"Is the bundle ACTIVE?<br/>/system/console/bundles"}
B -->|"INSTALLED"| C\["Unsatisfied Import-Package<br/>— see file 01"]
B -->|"ACTIVE"| D\{"Is the component listed?<br/>/system/console/components"}
D -->|"Not listed"| E\["Annotation missing, or the class<br/>wasn't in the deployed bundle"]
D -->|"Unsatisfied"| F\{"Which reference is missing?"}
F --> G\["A MANDATORY @Reference<br/>whose service isn't registered"]
F --> H\["configurationPolicy = REQUIRE<br/>with no config deployed"]
D -->|"Active"| I\["The component IS running —<br/>the problem is elsewhere"]
⛶ Fullscreen
The components console tells you exactly which reference is unsatisfied. That is the single most useful screen in AEM for this class of problem — it names the missing service rather than making you guess.

A subtle cascade worth knowing: if service A is unsatisfied, then every component with a mandatory reference to A also becomes unsatisfied, and so on. One missing configuration can silently disable a chain of six components. So when you see several unsatisfied components, look for the one at the root rather than fixing them individually.

3.4 Service ranking — choosing between implementations
When two implementations of the same interface are registered, which one does a single-cardinality @Reference get?

The highest service.ranking. Default is 0, and ties are broken by service ID — meaning the oldest registered wins, which is not something you want to depend on.

@Component(
service = ProductDataService.class,
property = \{ "service.ranking:Integer=100" }
)
public class CachedProductDataServiceImpl implements ProductDataService \{ }Copy
Copy
A genuinely useful pattern: ship a default implementation at ranking 0, and let a project-specific one override it at ranking 100 without either bundle knowing about the other. That is how a lot of AEM's own extensibility works.

And a caution: if two implementations end up registered and you did not intend it, you get behaviour that depends on ranking and registration order. /system/console/services shows what is actually registered under each interface, which is how you confirm.

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
8.1 The interface — the contract
package com.energy.core.services;

import java.util.List;
import java.util.Optional;

/\*\*

- Reads product data from the internal product API.
-
- <p>Implementations must never throw for an unknown product or an
- unavailable upstream — callers render pages and a failure here must
- degrade the component, not break the page.
  \*/
  public interface ProductDataService \{

  /\*\*

  - @return the specifications, or an EMPTY list if the product is
  - ```
        unknown or the upstream service is unavailable.
    ```

  \*/
  List<Specification> getSpecifications(String productId);

  /\*\*

  - @return the product, or empty if unknown/unavailable.
    \*/
    Optional<Product> getProduct(String productId);

  /\*\*

  - @return false if the integration is disabled or the upstream is
  - ```
        currently failing. Components can use this to decide
    ```
  - ```
        whether to render a degraded state.
    ```

  \*/
  boolean isAvailable();
  }Copy
  Copy
  Note the Javadoc is doing real work. It documents the failure behaviour, which is exactly the thing that gets lost when people skip the interface.

8.2 The configuration definition
package com.energy.core.services.impl;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

/\*\*

- Note this is an @interface -- a Java annotation type.
- Each method becomes a configuration property; `default` is its fallback.
  \*/
  @ObjectClassDefinition(
  name = "Energy — Product Data Service",
  description = "Connection and caching settings for the internal product API"
  )
  public @interface ProductDataServiceConfig \{

  @AttributeDefinition(
  name = "API Endpoint",
  description = "Base URL of the product API",
  type = AttributeType.STRING
  )
  String apiEndpoint() default "[https://api.internal/v1](https://api.internal/v1)";

  @AttributeDefinition(
  name = "Connect Timeout (ms)",
  description = "Give up if the connection is not established in this time"
  )
  int connectTimeout() default 3000;

  @AttributeDefinition(
  name = "Socket Timeout (ms)",
  description = "Give up if no data arrives in this time"
  )
  int socketTimeout() default 5000;

  @AttributeDefinition(
  name = "Cache TTL (seconds)",
  description = "How long to reuse a response. 0 disables caching."
  )
  int cacheTtl() default 300;

  @AttributeDefinition(
  name = "Enabled",
  description = "Turn the integration off without a deployment"
  )
  boolean enabled() default true;
  }Copy
  Copy
  8.3 The implementation — every annotation in context
  package com.energy.core.services.impl;

import com.energy.core.services.ProductDataService;
import com.energy.core.services.Specification;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

@Component(
// Publish under the INTERFACE, not this class
service = ProductDataService.class,

```
    // Meaningless without an endpoint, so refuse to activate
    // without configuration rather than run half-broken.
    // NOTE: a missing config file then means SILENT non-activation.
    configurationPolicy = ConfigurationPolicy.REQUIRE
```

)
@Designate(ocd = ProductDataServiceConfig.class)
public class ProductDataServiceImpl implements ProductDataService \{

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

/**
 * MANDATORY by default: this component will not activate at all
 * if no ResourceResolverFactory is registered.
 */
@Reference
private org.apache.sling.api.resource.ResourceResolverFactory resolverFactory;

/**
 * OPTIONAL + DYNAMIC: analytics is a nice-to-have. Without this,
 * a missing analytics service would stop the whole product
 * integration from activating.
 *
 * volatile is REQUIRED with DYNAMIC -- another thread rebinds it
 * while request threads are reading it.
 */
@Reference(cardinality = ReferenceCardinality.OPTIONAL,
           policy = ReferencePolicy.DYNAMIC)
private volatile AnalyticsService analyticsService;

// Set once in @Activate, never mutated afterwards.
// This service is a SINGLETON shared across every request thread,
// so anything mutable here would be a race condition.
private String apiEndpoint;
private boolean enabled;
private CloseableHttpClient httpClient;

/**
 * @Activate and @Modified point at the SAME method.
 *
 * Without @Modified, a config change would deactivate and
 * reactivate the component, tearing down and rebuilding the
 * HTTP connection pool. With it, we just reconfigure in place.
 */
@Activate
@Modified
protected void activate(ProductDataServiceConfig config) {
    this.apiEndpoint = config.apiEndpoint();
    this.enabled     = config.enabled();

    // Close any previous client before replacing it (@Modified path)
    closeClient();

    // TIMEOUTS ARE NOT OPTIONAL. Without a socket timeout, a
    // hung upstream consumes every request thread on the instance.
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(config.connectTimeout())
            .setSocketTimeout(config.socketTimeout())
            .build();

    this.httpClient = HttpClients.custom()
            .setDefaultRequestConfig(requestConfig)
            .build();

    LOG.info("Product data service activated. endpoint={} enabled={}",
            apiEndpoint, enabled);
}

/**
 * Release everything. Components stop and start on EVERY redeploy,
 * so skipping this leaks a connection pool per deployment.
 */
@Deactivate
protected void deactivate() {
    closeClient();
    LOG.info("Product data service deactivated");
}

private void closeClient() {
    if (httpClient != null) {
        try {
            httpClient.close();
        } catch (IOException e) {
            LOG.warn("Failed to close HTTP client", e);
        }
        httpClient = null;
    }
}

@Override
public List<Specification> getSpecifications(String productId) {
    if (!enabled) {
        return Collections.emptyList();
    }
    try {
        List<Specification> specs = fetchSpecifications(productId);

        // Null-check: OPTIONAL reference may genuinely be absent
        if (analyticsService != null) {
            analyticsService.trackLookup(productId);
        }
        return specs;

    } catch (Exception e) {
        // NEVER propagate. The contract says degrade, don't break.
        LOG.warn("Product lookup failed for {}: {}", productId, e.getMessage());
        return Collections.emptyList();
    }
}

@Override
public boolean isAvailable() {
    return enabled && httpClient != null;
}

private List<Specification> fetchSpecifications(String productId) {
    // productId stays a LOCAL/parameter value -- never an instance
    // field, because this singleton is shared across threads.
    return Collections.emptyList();   // real HTTP call omitted
}
```

}Copy
Copy
The seven decisions to be able to defend:

service = ProductDataService.class publishes under the interface, so consumers never see this class.

configurationPolicy = REQUIRE means no half-configured activation — but you must know it makes a missing config a silent failure.

@Activate and @Modified on one method, so a config change reconfigures rather than rebuilds.

@Deactivate closes the client. Components restart on every redeploy.

Timeouts always. A missing socket timeout is how every request thread ends up hung.

The OPTIONAL reference is volatile and null-checked.

No mutable request state in instance fields, because this singleton is shared across threads.

8.4 The configuration files, per run mode
ui.config/.../apps/energy/osgiconfig/config.author.dev/com.energy.core.services.impl.ProductDataServiceImpl.cfg.json

\{
"apiEndpoint": "[https://api-test.internal/v1](https://api-test.internal/v1)",
"connectTimeout": 5000,
"socketTimeout": 10000,
"cacheTtl": 0,
"enabled": true
}Copy
Copy
ui.config/.../apps/energy/osgiconfig/config.publish.prod/com.energy.core.services.impl.ProductDataServiceImpl.cfg.json

\{
"apiEndpoint": "[https://api.internal.company.com/v2](https://api.internal.company.com/v2)",
"connectTimeout": 3000,
"socketTimeout": 5000,
"cacheTtl": 600,
"enabled": true
}Copy
Copy
Two rules that cause silent failures when broken:

The filename must exactly match the component's PID — the fully-qualified class name. A typo deploys fine and is never applied, with no error at all.

The folder name selects the run mode. Dev gets the test API and longer timeouts with caching off for easier debugging; production gets the live API, tighter timeouts and a ten-minute cache.

8.5 Consuming it — from a servlet and from a model
From a servlet — @Reference, syllabus point 11:

package com.energy.core.servlets;

import com.energy.core.services.ProductDataService;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.servlet.Servlet;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "energy/components/productdetail",
selectors = "specs",
extensions = "json",
methods = "GET"
)
public class ProductSpecsServlet extends SlingSafeMethodsServlet \{

```
/**
 * @Reference works here because the SERVLET IS an OSGi component --
 * @SlingServletResourceTypes is a wrapper over @Component.
 * MANDATORY by default: no service, no servlet.
 */
@Reference
private ProductDataService productDataService;

// ... doGet
```

}Copy
Copy
From a Sling Model — @OSGiService, syllabus point 10, for contrast:

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

```
/**
 * @OSGiService, NOT @Reference.
 *
 * A Sling Model is created by the Sling Models framework, not by
 * Declarative Services -- so DS never sees this class and cannot
 * inject into it.
 */
@OSGiService
private ProductDataService productDataService;
```

}Copy
Copy
Being able to show these two side by side is the cleanest possible answer to syllabus points 10 and 11.

8.6 A component that is not a service
package com.energy.core.startup;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;

/\*\*

- No `service` attribute → this is a COMPONENT but NOT a SERVICE.
- Nothing can @Reference it. It exists to do work at startup.
-
- immediate = true because a component that provides no service would
- otherwise have nothing to trigger its activation.
  \*/
  @Component(immediate = true)
  public class CacheWarmupTask \{

  @Activate
  protected void activate() \{
  // warm up an internal cache at startup
  }
  }Copy
  Copy
  This is the concrete example to give when asked "give me a component that isn't a service."

8.7 Unit testing a service
package com.energy.core.services.impl;

import com.energy.core.services.ProductDataService;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.\*;

@ExtendWith(AemContextExtension.class)
class ProductDataServiceImplTest \{

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

private ProductDataService service;

@BeforeEach
void setUp() {
    // Register the component WITH configuration -- required,
    // because configurationPolicy = REQUIRE means it will not
    // activate without one.
    Map<String, Object> config = new HashMap<>();
    config.put("apiEndpoint", "https://test.local/v1");
    config.put("connectTimeout", 1000);
    config.put("socketTimeout", 1000);
    config.put("enabled", true);

    service = context.registerInjectActivateService(
            new ProductDataServiceImpl(), config);
}

@Test
void returnsEmptyListWhenDisabled() {
    Map<String, Object> disabled = new HashMap<>();
    disabled.put("apiEndpoint", "https://test.local/v1");
    disabled.put("enabled", false);

    ProductDataService off = context.registerInjectActivateService(
            new ProductDataServiceImpl(), disabled);

    assertTrue(off.getSpecifications("P-1").isEmpty());
    assertFalse(off.isAvailable());
}

@Test
void neverThrowsForAnUnknownProduct() {
    // The contract says degrade, don't break.
    assertNotNull(service.getSpecifications("does-not-exist"));
}
```

}Copy
Copy
context.registerInjectActivateService is the method to remember — it registers the component, injects its references, and calls @Activate with the configuration you supply, in one call.

9. Common Mistakes
   The mistake	What actually happens	The fix
   No service attribute when you meant a service	Nothing can @Reference it; consumers stay unsatisfied	service = MyService.class
   Registering under the impl class instead of the interface	Consumers coupled to the implementation	Register under the interface
   Exporting the impl package	Module boundary gone; others depend on internals	Export the interface package only
   configurationPolicy = REQUIRE with no config deployed	Silent non-activation — no error at all	Deploy the config, or use OPTIONAL with defaults
   Config filename not matching the PID	Deploys fine, silently never applied	Filename = fully-qualified class name
   Config in the wrong run-mode folder	Works in one environment, not another	Check the folder name
   Missing @Deactivate	Leaks a connection pool or thread pool on every redeploy	Release everything you hold
   Missing @Modified	A config change tears down and rebuilds the component	Point @Modified at the same method
   Mutable instance field holding request state	Race condition — passes tests, fails under load	Local variables and parameters only
   DYNAMIC policy without volatile	Threads may read a stale reference	Make the field volatile
   Not null-checking an OPTIONAL reference	Intermittent NPE when the service is absent	Always null-check
   MANDATORY for a genuinely optional dependency	One missing service disables the whole feature	OPTIONAL + DYNAMIC + volatile
   No timeouts on an external call	Every request thread hangs on an unresponsive upstream	Connect and socket timeouts
   Throwing out of a service used during rendering	Breaks the page instead of degrading the component	Return an empty result, log at warn
   Using deprecated Felix @Service / @Property	Unsupported in newer AEM	service attribute and @ObjectClassDefinition
   @Reference in a Sling Model	Never injected — DS doesn't manage models	@OSGiService
10. Best Practices
    On structure. Always an interface, in an exported package, with the implementation in a non-exported impl package. Document failure behaviour on the interface, because that is where it will actually be read.

On configuration. Use @ObjectClassDefinition with sensible defaults, so the service works out of the box and configuration only overrides. Ship values as run-mode JSON in ui.config. Include an enabled flag on any external integration, so it can be switched off without a deployment.

On lifecycle. Build expensive things in @Activate, release them in @Deactivate, and point @Modified at the same method as @Activate unless you have a reason not to.

On references. MANDATORY only for things the component genuinely cannot function without. Optional integrations get OPTIONAL + DYNAMIC + volatile + a null check.

On thread safety. Treat every singleton service as shared across all request threads. Fields immutable after activation. Nothing request-specific in a field, ever.

On resilience. Timeouts on every outbound call. Never throw out of a service used during rendering — return an empty result and log at warn. Consider a health check so an unsatisfied or failing service is visible rather than silent.

On testing. registerInjectActivateService with configuration. Test the disabled path and the upstream-failure path, because those are the ones that matter in production and the ones nobody writes.

11. Debugging Tips
    The two consoles that answer almost everything:

/system/console/bundles — is the bundle even ACTIVE? Stuck in INSTALLED means an unsatisfied Import-Package, which is a bundle-level problem (file 01).

/system/console/components — is the component Active, or Unsatisfied? And crucially, it names the reference that is missing. That turns guesswork into a single reading.

Getting the distinction right matters, and it is worth stating in an interview:

Symptom	Level	Where to look
Bundle stuck in INSTALLED	Bundle	/system/console/bundles — unsatisfied import
Component Unsatisfied	Component	/system/console/components — missing reference or config
Component Active but wrong behaviour	Configuration	/system/console/configMgr — effective values
Two implementations, wrong one used	Registry	/system/console/services — what's registered and its ranking
Following the cascade. Several unsatisfied components usually mean one root cause. Trace each missing reference back until you find a component whose own references are all satisfied — that one is the actual problem, and it is typically a missing configuration or a bundle that failed to resolve.

When a configuration seems not to apply:

Open /system/console/configMgr and find the component — it shows the effective values.
If the values are defaults, the config file was never matched. Check the filename against the PID, character for character.
Check the run-mode folder name.
Check the file actually deployed — is it in the package filter?
When a component is Active but not doing anything: it is running, so the problem is inside your code, not in OSGi. Add a package-scoped DEBUG logger at /system/console/slinglog rather than guessing.

Console	Answers
/system/console/bundles	Is the bundle ACTIVE? Which import is unsatisfied?
/system/console/components	Active or Unsatisfied — and which reference is missing
/system/console/configMgr	What configuration values are actually in effect
/system/console/services	What's registered under an interface, with ranking
/system/console/slinglog	Add a DEBUG logger for just your package
/system/console/depfinder	Which bundle exports a package you need
12\. Performance Optimization
Timeouts on every outbound call. This is the highest-impact item on the page. A missing socket timeout means request threads block indefinitely on an unresponsive upstream, and once the thread pool is exhausted the entire publish instance stops responding — not just the affected feature.

Cache in the service, not in the model. A service is a natural caching layer because it is a singleton with a lifecycle. Set the TTL from configuration so it can be tuned per environment.

Build expensive resources once, in @Activate. An HTTP client with a connection pool should be created once, not per call. And use @Modified so a config change does not rebuild it.

Do not do heavy work in @Activate on a component that activates during startup — it delays the instance becoming available. Warm caches asynchronously if they take real time.

Watch reference cardinality. A STATIC MANDATORY reference means the component is torn down and rebuilt whenever that service restarts. For something that restarts often, DYNAMIC avoids the churn.

Mind the singleton. One instance serving all threads means any synchronisation you add is a global bottleneck. Prefer immutable state over locking.

2 sections hidden in Learn mode
13\. Real Production Scenarios · 14. Follow-up Questions
Show
15\. Comparison Tables
Component vs Service — syllabus point 13

Component	Service
Annotation	@Component	@Component(service = X.class)
Managed by DS	Yes	Yes
Has a lifecycle	Yes	Yes
In the service registry	No	Yes
Others can @Reference it	No	Yes
Relationship	—	Every service is a component
@Reference vs @OSGiService — syllabus points 10 and 11

@Reference	@OSGiService
Used in	An OSGi @Component	A Sling @Model
Injected by	Declarative Services	Sling Models framework
Servlets, filters, services	Yes	No
Component models	No	Yes
Filtering attribute	target	filter
Reference cardinality

Cardinality	Count	Component activates without it?
MANDATORY (default)	1..1	No
OPTIONAL	0..1	Yes
MULTIPLE	0..n	Yes
AT\_LEAST\_ONE	1..n	No
STATIC vs DYNAMIC policy

STATIC (default)	DYNAMIC
Service changes	Component deactivates and reactivates	Reference swapped in place
Field must be volatile	No	Yes
Component state preserved	No	Yes
Use for	Stable mandatory dependencies	Optional or multiple references
configurationPolicy

Value	Behaviour	Risk
OPTIONAL (default)	Activates with or without config	May run with defaults you didn't intend
REQUIRE	Only activates with config	Silent non-activation if config is missing
IGNORE	Ignores configuration	No configurability
Old vs new annotations

Old (Felix SCR, deprecated)	Current (OSGi DS)
@Service (separate)	service attribute on @Component
@Property	@ObjectClassDefinition + @AttributeDefinition
org.apache.felix.scr.annotations	org.osgi.service.component.annotations
Bundle state vs Component state

Bundle	Component
States	INSTALLED, RESOLVED, ACTIVE…	Unsatisfied, Satisfied, Active
Stuck because	Unsatisfied Import-Package	Missing @Reference or config
Console	/system/console/bundles	/system/console/components
