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
- Declaring How do I tell OSGi this class exists?
- Lifecycle How do I run code when it starts and stops?
- Injection How do I get hold of another service?
- Configuration How do I make settings change per environment?
- 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.
- 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
@Component + @Reference"] -->|"DS reads
the annotations"| B["DS runtime"]
B -->|"creates the instance"| C["Your object"]
B -->|"publishes to"| D["Service registry
(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.
- 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
DS manages it, gives it a lifecycle"] --> B{"Does it have
service = ...?"}
B -->|No| C["A COMPONENT
runs, but invisible to others"]
B -->|Yes| D["A COMPONENT and A SERVICE
published on the noticeboard
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 {
}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 {
}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
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 {
}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 {
}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 {
}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", "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.MyServiceImplproducts.cfg.json
com.energy.core.services.impl.MyServiceImpldownloads.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 {
}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.
- Internal Working
3.1 The component lifecycle
stateDiagram-v2
[] --> Disabled
Disabled --> Unsatisfied: bundle started, component enabled
Unsatisfied --> Satisfied: all MANDATORY references available
+ 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
⛶ 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,
no errors in the log"] --> B{"Is the bundle ACTIVE?
/system/console/bundles"}
B -->|"INSTALLED"| C["Unsatisfied Import-Package
— see file 01"]
B -->|"ACTIVE"| D{"Is the component listed?
/system/console/components"}
D -->|"Not listed"| E["Annotation missing, or the class
wasn't in the deployed bundle"]
D -->|"Unsatisfied"| F{"Which reference is missing?"}
F --> G["A MANDATORY @Reference
whose service isn't registered"]
F --> H["configurationPolicy = REQUIRE
with no config deployed"]
D -->|"Active"| I["The component IS running —
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.
-
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
-
*/ List
getSpecifications(String productId); /**
- @return the product, or empty if unknown/unavailable.
*/
Optional
getProduct(String productId);
/**
- @return false if the integration is disabled or the upstream is
-
-
*/ 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;
defaultis 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";
@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,
) @Designate(ocd = ProductDataServiceConfig.class) public class ProductDataServiceImpl implements ProductDataService {
}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", "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", "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 {
}Copy Copy From a Sling Model — @OSGiService, syllabus point 10, for contrast:
@Model(adaptables = Resource.class) public class ProductModel {
}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
serviceattribute → 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 {
}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.
- 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
- 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.
- 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