trivias) {
+ return trivias.stream().anyMatch(SingletonInsteadOfApplicationScopedCheck::isSingletonComment);
+ }
+
+ private static boolean isSingletonComment(SyntaxTrivia trivia) {
+ return trivia.comment().toLowerCase(Locale.ROOT).contains("singleton");
+ }
+}
diff --git a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
new file mode 100644
index 00000000000..4bdf743b52e
--- /dev/null
+++ b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube Java
+ * Copyright (C) SonarSource Sàrl
+ * mailto:info AT sonarsource DOT com
+ *
+ * You can redistribute and/or modify this program under the terms of
+ * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the Sonar Source-Available License for more details.
+ *
+ * You should have received a copy of the Sonar Source-Available License
+ * along with this program; if not, see https://sonarsource.com/license/ssal/
+ */
+package org.sonar.java.checks.quarkus;
+
+import org.junit.jupiter.api.Test;
+import org.sonar.java.checks.verifier.CheckVerifier;
+
+import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPathInModule;
+import static org.sonar.java.test.classpath.TestClasspathUtils.QUARKUS_ARC_315_MODULE;
+
+class SingletonInsteadOfApplicationScopedCheckTest {
+
+ private static final String SAMPLE = mainCodeSourcesPathInModule(QUARKUS_ARC_315_MODULE, "checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java");
+
+ @Test
+ void test() {
+ CheckVerifier.newVerifier()
+ .onFile(SAMPLE)
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withClassPath(QUARKUS_ARC_315_MODULE.getClassPath())
+ .verifyIssues();
+ }
+
+ @Test
+ void testWithoutSemantic() {
+ CheckVerifier.newVerifier()
+ .onFile(SAMPLE)
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withoutSemantic()
+ .verifyNoIssues();
+ }
+
+ @Test
+ void testWithoutQuarkusOnClasspath() {
+ CheckVerifier.newVerifier()
+ .onFile(SAMPLE)
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withClassPath(java.util.List.of())
+ .verifyNoIssues();
+ }
+}
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
new file mode 100644
index 00000000000..b3ca8a4ebcc
--- /dev/null
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
@@ -0,0 +1,114 @@
+This rule raises an issue when a CDI bean uses the @Singleton annotation from jakarta.inject instead of
+@ApplicationScoped in a Quarkus application.
+Why is this an issue?
+In Quarkus applications, both @Singleton (from Jakarta Dependency Injection) and @ApplicationScoped (from Jakarta
+Contexts and Dependency Injection) create single-instance beans. However, they behave differently in important ways.
+@ApplicationScoped beans are full CDI beans with these capabilities:
+
+ - Support lazy initialization via client proxies
+ - Can be mocked in tests using QuarkusMock
+ - Can be destroyed and recreated at runtime thanks to proxy-based method delegation
+
+@Singleton beans are simpler but more limited:
+
+ - Created eagerly when the bean is injected
+ - Cannot be mocked using QuarkusMock in tests
+
+For most application-level services, @ApplicationScoped is the better choice because it provides the full CDI feature set that Quarkus
+applications typically need.
+When @Singleton might be acceptable
+@Singleton might have a slightly better performance than @ApplicationScoped because of the absence of client proxy. Thus,
+there are specific scenarios where @Singleton may be intentionally chosen:
+
+ - Low-level infrastructure beans: Beans that provide basic utilities and don’t require lazy initialization
+ - Performance-critical paths: Beans where the proxy overhead of
@ApplicationScoped is measured and found to be
+ significant (rare, and should be documented)
+ - Third-party integration: When integrating libraries that specifically expect non-proxied instances
+
+If you choose to use @Singleton, document why you need it and confirm that:
+
+ - The bean does not need to be mocked in tests, or you use alternative mocking strategies
+ - You understand the limitations and accept them for this specific case
+
+What is the potential impact?
+Using @Singleton instead of @ApplicationScoped can lead to several issues:
+Testing difficulties
+@Singleton beans cannot be mocked using QuarkusMock in tests. This means:
+
+ - Integration tests cannot isolate the bean for testing
+ - External dependencies (databases, APIs) cannot be easily stubbed
+ - Tests may become slower and more fragile
+ - Test coverage may be reduced
+
+Reduced flexibility
+Choosing @Singleton limits your future options:
+
+ - Lazy initialization via proxies is unavailable
+
+How to fix it in Quarkus
+Replace the @Singleton annotation with @ApplicationScoped. Update the import statement to use
+jakarta.enterprise.context.ApplicationScoped instead of jakarta.inject.Singleton.
+Code examples
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant
+public class UserService {
+ public String getUser(Long id) {
+ // implementation
+ return "user";
+ }
+}
+
+Compliant solution
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class UserService {
+ public String getUser(Long id) {
+ // implementation
+ return "user";
+ }
+}
+
+If you have a documented, valid reason to use @Singleton, you can keep it but should add a comment explaining why. This is an
+alternative to changing the annotation, though it’s only appropriate in specific cases.
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant
+public class ApplicationConfig {
+ private final String version = "1.0.0";
+
+ public String getVersion() {
+ return version;
+ }
+}
+
+Compliant solution
+
+import jakarta.inject.Singleton;
+
+// Using @Singleton intentionally: this is a simple configuration holder
+// with no interceptor requirements and no need for testing isolation.
+// It's instantiated eagerly at startup to validate configuration.
+@Singleton
+public class ApplicationConfig {
+ private final String version = "1.0.0";
+
+ public String getVersion() {
+ return version;
+ }
+}
+
+Resources
+Documentation
+
+
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json
new file mode 100644
index 00000000000..5e9dfbe8d80
--- /dev/null
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json
@@ -0,0 +1,25 @@
+{
+ "title": "\"@ApplicationScoped\" should be preferred over \"@Singleton\" in Quarkus applications",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5 min"
+ },
+ "tags": [
+ "jee",
+ "injection",
+ "testing"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-9068",
+ "sqKey": "S9068",
+ "scope": "All",
+ "quickfix": "unknown",
+ "code": {
+ "impacts": {
+ "MAINTAINABILITY": "MEDIUM"
+ },
+ "attribute": "MODULAR"
+ }
+}
diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9068 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9068
new file mode 100644
index 00000000000..e69de29bb2d