Code Quality Metrics in Java: SonarQube, Cyclomatic Complexity, Technical Debt & Maintainability in 2026
Code quality is not a feeling — it is a set of measurable, enforceable properties. In 2026, engineering teams that ship reliable Java microservices are the ones that have operationalized quality metrics: they track cyclomatic complexity per method, enforce coverage thresholds in CI, measure technical debt in hours, and fail builds when SonarQube quality gates are not met. This guide covers every major metric, shows you how to configure SonarQube and JaCoCo in Spring Boot, and demonstrates how to integrate quality enforcement into GitHub Actions pipelines that protect your main branch.
Table of Contents
- Why Code Quality Metrics Matter in Production
- The Core Quality Metrics Explained
- Cyclomatic Complexity — Measurement & Reduction
- Code Coverage with JaCoCo in Spring Boot
- SonarQube Setup for Java Spring Boot Projects
- SonarQube Quality Gates — Configuration & Enforcement
- Technical Debt — Calculation & Remediation
- Code Duplication — Detection & Fixes
- Integrating Quality Gates in CI/CD (GitHub Actions)
- Building a Quality Culture — Team Practices
1. Why Code Quality Metrics Matter in Production
A CAST Research report estimated that the global cost of poor software quality exceeds $3.61 trillion in technical debt. That staggering figure has a micro-level translation every Java team experiences: a new feature request takes two weeks instead of two days because the codebase is tangled, a production bug surfaces because an edge case was untested, a microservice fails its deployment because a cyclomatic complexity spike was never caught in review. Code quality metrics convert these vague anxieties into measurable, actionable numbers.
Quality metrics provide four essential capabilities. First, they surface problems before they reach production — a method with cyclomatic complexity of 25 is almost certainly a bug magnet. Second, they create an objective Definition of Done — a pull request that drops coverage below 80% is not done, regardless of how the feature looks. Third, they enable trend analysis — watching technical debt grow from 2 days to 3 weeks over six sprints tells you something is wrong before it becomes a crisis. Fourth, they justify refactoring investment — showing stakeholders that paying down 40 hours of debt will reduce bug rate by 30% based on historical correlation is far more persuasive than a subjective "the code needs cleanup."
The following table summarizes the key quality metrics, what each one measures, and what target value to aim for in a production Spring Boot microservice:
| Quality Metric | What It Measures | Target Value |
|---|---|---|
| Cyclomatic Complexity | Number of independent execution paths | ≤ 5 per method |
| Code Coverage (Line) | % of source lines executed by tests | ≥ 80% |
| Code Coverage (Branch) | % of decision branches executed | ≥ 70% |
| Technical Debt Ratio | Remediation cost / development cost | ≤ 5% (Rating A) |
| Code Duplication | % of duplicate code blocks | ≤ 3% |
| Bugs (Critical/Blocker) | Confirmed defects in production paths | 0 |
| Vulnerabilities | Security weaknesses exploitable by attackers | 0 Critical/High |
2. The Core Quality Metrics Explained
Before diving into tooling, it is worth precisely understanding what each metric measures and why it matters in the context of a Java Spring Boot application.
Cyclomatic Complexity (CC): Developed by Thomas McCabe in 1976, CC is computed as E - N + 2P where E is the number of edges in the control flow graph, N is the number of nodes, and P is the number of connected components (typically 1 for a single method). Practically, it equals 1 plus the number of decision points (if, while, for, case, catch, &&, ||). A method with CC=1 has a single linear path; CC=10 means 10 independent paths, each requiring its own test case for full branch coverage.
Cognitive Complexity: SonarQube's modern alternative to CC. While CC counts control structures, Cognitive Complexity penalises nesting depth — a nested loop inside a nested if inside a try/catch scores much higher than three sequential if statements. It better correlates with human comprehension difficulty, which is why SonarQube uses it as its primary complexity metric since SonarQube 7.1.
Lines of Code (LOC) vs Effective Lines of Code (ELOC): LOC counts all lines including blank lines and comments. ELOC (or NCLOC — non-commenting lines of code) counts only executable statements. SonarQube reports NCLOC, which is the meaningful size metric for a service. A 10,000-NCLOC service is large; a 500-NCLOC service is small.
Code Duplication %: The percentage of NCLOC that appears in duplicate blocks. SonarQube flags a block as duplicated when 10 or more consecutive lines are identical (after normalisation for variable names). Above 5% duplication is a strong indicator of copy-paste programming and divergent bug fixes. The standard target for production services is below 3%.
Technical Debt: Measured using the SQALE (Software Quality Assessment based on Lifecycle Expectations) methodology. SonarQube calculates remediation time for each code smell, bug, and vulnerability, sums them up, and computes the debt ratio: (remediation cost / development cost) × 100%. Development cost is estimated at 30 minutes per NCLOC.
Test Coverage: Line coverage measures what percentage of executable lines are hit by at least one test. Branch coverage measures what percentage of decision branches (both the true and false paths of every if/switch/ternary) are exercised. Branch coverage is stricter and more valuable — you can have 100% line coverage with 50% branch coverage by never testing the false path of a critical null check.
Bugs vs Vulnerabilities vs Code Smells: In SonarQube's taxonomy these are distinct. Bugs are issues that represent wrong behaviour — likely to produce an incorrect result. Vulnerabilities are security weaknesses that could be exploited. Code smells are maintainability issues that do not break behaviour but increase the chance of bugs in future modifications. Bugs and vulnerabilities affect the Reliability and Security ratings; code smells affect the Maintainability rating and accumulate as technical debt.
| Metric | Good | Acceptable | Bad |
|---|---|---|---|
| Cyclomatic Complexity / method | 1–5 | 6–10 | 11+ |
| Line Coverage | ≥ 80% | 60–79% | < 60% |
| Technical Debt Ratio | ≤ 5% | 6–20% | > 20% |
| Duplication | ≤ 3% | 4–10% | > 10% |
| Blocker/Critical Bugs | 0 | 1–3 minor | Any blocker |
3. Cyclomatic Complexity — Measurement & Reduction
Cyclomatic complexity is the most actionable structural metric available to Java engineers. Every branching point — if, else if, for, while, do-while, case, catch, &&, ||, ternary operator — increments the complexity by one. Understanding CC lets you reason directly about testability: a CC=8 method theoretically requires 8 test cases for complete path coverage.
Consider an OrderService.calculateDiscount() method that handles multiple customer tiers and promotional flags. A naive implementation that chain-conditions all the rules into a single method quickly reaches CC=8 or higher:
// HIGH COMPLEXITY: CC = 8 — hard to test, hard to extend
@Service
public class OrderService {
public BigDecimal calculateDiscount(Order order, Customer customer) {
BigDecimal discount = BigDecimal.ZERO;
if (customer.getTier() == CustomerTier.GOLD) { // +1
discount = discount.add(new BigDecimal("0.10"));
if (order.getTotal().compareTo(new BigDecimal("500")) >= 0) { // +1
discount = discount.add(new BigDecimal("0.05"));
}
} else if (customer.getTier() == CustomerTier.SILVER) { // +1
discount = discount.add(new BigDecimal("0.05"));
}
if (order.isFirstOrder() && customer.isNewCustomer()) { // +1 +1
discount = discount.add(new BigDecimal("0.15"));
}
if (order.hasPromoCode() && !order.isAlreadyDiscounted()) { // +1 +1
discount = discount.add(new BigDecimal("0.08"));
}
return discount.min(new BigDecimal("0.30")); // cap at 30%
}
}The refactored version extracts each discount rule into its own method (CC=2 each) and uses a composable rule pattern, making each rule independently testable:
// REFACTORED: CC = 2 per method — each rule is independently testable
@Service
@RequiredArgsConstructor
public class OrderDiscountService {
private final List<DiscountRule> discountRules;
public BigDecimal calculateDiscount(Order order, Customer customer) {
return discountRules.stream()
.map(rule -> rule.apply(order, customer))
.reduce(BigDecimal.ZERO, BigDecimal::add)
.min(new BigDecimal("0.30"));
}
}
@FunctionalInterface
public interface DiscountRule {
BigDecimal apply(Order order, Customer customer);
}
@Component
public class GoldTierDiscount implements DiscountRule {
@Override
public BigDecimal apply(Order order, Customer customer) {
if (customer.getTier() != CustomerTier.GOLD) return BigDecimal.ZERO; // +1
BigDecimal base = new BigDecimal("0.10");
return order.getTotal().compareTo(new BigDecimal("500")) >= 0
? base.add(new BigDecimal("0.05")) : base;
}
}
@Component
public class NewCustomerDiscount implements DiscountRule {
@Override
public BigDecimal apply(Order order, Customer customer) {
return (order.isFirstOrder() && customer.isNewCustomer()) // +1
? new BigDecimal("0.15") : BigDecimal.ZERO;
}
}
@Component
public class PromoCodeDiscount implements DiscountRule {
@Override
public BigDecimal apply(Order order, Customer customer) {
return (order.hasPromoCode() && !order.isAlreadyDiscounted()) // +1
? new BigDecimal("0.08") : BigDecimal.ZERO;
}
}| CC Range | Risk Level | Action | Tool Alert |
|---|---|---|---|
| 1–5 | Low | None required | Green |
| 6–10 | Moderate | Flag in code review | Yellow |
| 11–20 | High | Refactor before merge | Orange / Code Smell |
| 21+ | Critical | Block PR, mandatory refactor | Red / Blocker |
CyclomaticComplexity rule), Checkstyle (CyclomaticComplexity module), IntelliJ IDEA built-in (Analyze → Calculate Metrics), JaCoCo (indirect via branch paths).4. Code Coverage with JaCoCo in Spring Boot
JaCoCo (Java Code Coverage) is the de-facto standard coverage tool for Java projects. It instruments bytecode at load time and produces line, branch, method, class, and instruction coverage reports in HTML, XML, and CSV formats. Integrating JaCoCo into a Spring Boot Maven project requires adding the plugin to pom.xml, binding it to the test lifecycle, and optionally configuring minimum thresholds that fail the build if not met.
<!-- pom.xml — JaCoCo Maven Plugin configuration -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<!-- Instrument classes before tests run -->
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Generate HTML/XML report after tests -->
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
<!-- Enforce minimum coverage thresholds -->
<execution>
<id>check</id>
<goals><goal>check</goal></goals>
<configuration>
<haltOnFailure>true</haltOnFailure>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
<!-- Exclude generated code, config, and DTO classes -->
<excludes>
<exclude>**/config/**</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/*Application.class</exclude>
<exclude>**/generated/**</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>Run tests and generate the coverage report with:
# Run tests + generate JaCoCo HTML report
mvn test jacoco:report
# Report location:
# target/site/jacoco/index.html
# To also enforce thresholds (fails build if below minimum):
mvn verifyLine vs Branch Coverage: To illustrate the difference, consider this service method:
public String classify(int score) {
if (score >= 90) { // branch: true/false
return "Excellent";
} else if (score >= 70) { // branch: true/false
return "Good";
}
return "Needs Improvement";
}
// Test that achieves 100% LINE coverage but only 50% BRANCH coverage:
@Test
void testClassify() {
assertEquals("Excellent", classify(95)); // hits line 2 — true branch of first if
assertEquals("Good", classify(75)); // hits line 4 — true branch of second if
// Missing: score < 70 path (false branch of second if) → "Needs Improvement"
// Missing: score between 0-69 bypassing first if false branch
}@lombok.Data or Lombok exclusions), Spring configuration classes (@Configuration, @Bean methods that just wire beans), auto-generated code (OpenAPI client stubs, JPA metamodels), and Spring Boot main class. Excluding these prevents inflated coverage metrics that mask real gaps.5. SonarQube Setup for Java Spring Boot Projects
SonarQube is the most comprehensive static analysis platform for Java. It analyses security vulnerabilities, bugs, code smells, duplication, complexity, and coverage in one unified dashboard. For local development and CI pipelines you can run SonarQube Community Edition via Docker, and push analysis results using the Maven SonarScanner.
Step 1: Run SonarQube locally with Docker Compose:
# docker-compose.yml
version: "3.8"
services:
sonarqube:
image: sonarqube:10.4-community
container_name: sonarqube
ports:
- "9000:9000"
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
depends_on:
- db
db:
image: postgres:15
container_name: sonarqube_db
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- postgresql:/var/lib/postgresql
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql:
postgresql_data:Start SonarQube with docker-compose up -d, then access it at http://localhost:9000 (default credentials: admin/admin). Create a project and generate a token.
Step 2: Configure sonar-project.properties in your project root:
# sonar-project.properties
sonar.projectKey=my-spring-boot-service
sonar.projectName=My Spring Boot Service
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.exclusions=**/config/**,**/dto/**,**/*Application.java,**/generated/**
sonar.test.exclusions=**/test/**
sonar.sourceEncoding=UTF-8Step 3: Add the SonarScanner Maven plugin to pom.xml:
<!-- In pom.xml <build><plugins> section -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.10.0.2594</version>
</plugin>Step 4: Run the full analysis:
# Run tests to generate JaCoCo XML, then push to SonarQube
mvn clean verify sonar:sonar \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=YOUR_SONAR_TOKEN
# For SonarCloud (hosted):
mvn clean verify sonar:sonar \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.organization=your-org-key \
-Dsonar.token=$SONAR_TOKEN6. SonarQube Quality Gates — Configuration & Enforcement
A Quality Gate is a set of conditions that a project must meet for an analysis to be marked as Passed. The default "Sonar Way" gate focuses on new code only (the "Clean as You Code" philosophy): any new code introduced must meet the conditions, without being blocked by legacy issues in code you haven't touched. This is the recommended approach for teams adopting quality enforcement in existing codebases.
The default Sonar Way conditions on new code are:
| Metric | Operator | Value | Scope |
|---|---|---|---|
| Coverage on New Code | is less than | 80% | New code |
| Duplicated Lines on New Code | is greater than | 3% | New code |
| Maintainability Rating on New Code | is worse than | A | New code |
| Reliability Rating on New Code | is worse than | A | New code |
| Security Rating on New Code | is worse than | A | New code |
To fail a CI pipeline build when the quality gate is not met, add the sonar.qualitygate.wait property to your Maven command. SonarQube will poll the analysis result and return a non-zero exit code if the gate fails:
mvn clean verify sonar:sonar \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300POST /api/qualitygates/create) or through the UI at Quality Gates → Create. Always assign your project to the correct gate; the default "Sonar Way" is assigned to all projects unless overridden.7. Technical Debt — Calculation & Remediation
SonarQube computes technical debt using the SQALE (Software Quality Assessment based on Lifecycle Expectations) methodology. Each code smell has a fixed remediation cost (e.g., "remove this duplicated block: 30 minutes", "rename this method to be more descriptive: 5 minutes"). The technical debt ratio is the total remediation cost expressed as a percentage of the estimated development cost of the codebase: debt_ratio = (sum_of_remediation_costs / (NCLOC × 30 min)) × 100.
SonarQube translates this ratio into letter grades:
| Rating | Debt Ratio | Meaning | Action |
|---|---|---|---|
| A | ≤ 5% | Excellent | Maintain |
| B | 6–10% | Good | Monitor |
| C | 11–20% | Moderate | Plan remediation |
| D | 21–50% | High risk | Refactoring sprint |
| E | > 50% | Critical | Consider rewrite |
Common Java patterns that accumulate high technical debt in SonarQube include methods with excessive cognitive complexity, unused imports and private fields, methods that are too long (SonarQube flags methods over 150 lines), classes with too many methods (over 35 public methods), and magic numbers in conditional logic:
// HIGH DEBT PATTERNS — each generates a code smell in SonarQube
// 1. Magic numbers (should be named constants)
if (retryCount > 3) { // SonarQube: "Replace 3 with a named constant"
throw new RetryExhaustedException();
}
// 2. Empty catch block
try {
process(event);
} catch (Exception e) { // SonarQube: "Add a comment or log or rethrow"
}
// 3. Unused private field
public class ReportService {
private String lastGeneratedAt; // SonarQube: "Remove this unused private field"
public Report generate() { return new Report(); }
}
// 4. Mutable static field (thread-safety debt)
public class CacheService {
public static List<String> cache = new ArrayList<>(); // SonarQube: blocker
}
// REMEDIATION — pay the debt:
private static final int MAX_RETRIES = 3;
if (retryCount > MAX_RETRIES) {
throw new RetryExhaustedException();
}
try {
process(event);
} catch (ProcessingException e) {
log.error("Event processing failed for id={}", event.getId(), e);
throw e;
}8. Code Duplication — Detection & Fixes
Code duplication is the silent debt multiplier: every bug found in a duplicated block must be fixed in N places, and every future enhancement must be replicated consistently. SonarQube detects duplication by comparing normalised code blocks of 10+ lines across all files in the project. PMD CPD (Copy/Paste Detector) provides command-line duplication detection as a complementary tool during local development.
Add PMD CPD to your Maven build:
<!-- pom.xml — PMD plugin with CPD -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.21.0</version>
<configuration>
<minimumTokens>100</minimumTokens>
<failOnViolation>true</failOnViolation>
<printFailingErrors>true</printFailingErrors>
<excludes>
<exclude>**/generated/**/*.java</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>cpd-check</goal>
</goals>
</execution>
</executions>
</plugin>A classic duplication scenario in e-commerce services: separate discount calculation logic duplicated in OrderService and CartService. The fix is to extract a shared utility:
// BEFORE — duplicated logic in two services
// In OrderService.java:
BigDecimal discounted = price.multiply(BigDecimal.ONE.subtract(
discount.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)));
if (discounted.compareTo(BigDecimal.ZERO) < 0) discounted = BigDecimal.ZERO;
// In CartService.java (identical block):
BigDecimal discounted = price.multiply(BigDecimal.ONE.subtract(
discount.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)));
if (discounted.compareTo(BigDecimal.ZERO) < 0) discounted = BigDecimal.ZERO;
// --------------------------------------------------
// AFTER — shared utility method
@Component
public class PricingUtils {
public BigDecimal applyDiscount(BigDecimal price, BigDecimal discountPercent) {
BigDecimal discounted = price.multiply(BigDecimal.ONE.subtract(
discountPercent.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)));
return discounted.max(BigDecimal.ZERO);
}
}
// Both OrderService and CartService inject PricingUtils and call:
BigDecimal discounted = pricingUtils.applyDiscount(price, discount);9. Integrating Quality Gates in CI/CD (GitHub Actions)
The most impactful way to enforce code quality is to make the build fail automatically when quality gates are not met. The following GitHub Actions workflow runs tests, generates JaCoCo coverage, pushes analysis to SonarCloud, and fails the pipeline if the quality gate is not passed. Branch protection rules then prevent merging any PR with a failed pipeline.
# .github/workflows/quality.yml
name: Code Quality Gate
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
quality:
name: Build, Test & Quality Gate
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for SonarQube blame information
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Run tests with JaCoCo coverage
run: mvn clean verify -B
- name: SonarQube analysis with quality gate check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
mvn sonar:sonar \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.organization=${{ vars.SONAR_ORG }} \
-Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} \
-Dsonar.token=${{ secrets.SONAR_TOKEN }} \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300 \
-B
- name: Upload JaCoCo report as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacoco/
retention-days: 7Configure branch protection in your GitHub repository settings (Settings → Branches → Branch protection rules for main):
- ✅ Require status checks to pass before merging
- ✅ Require the quality / Build, Test & Quality Gate check to pass
- ✅ Require branches to be up to date before merging
- ✅ Require pull request reviews before merging (minimum 1 approval)
- ✅ Dismiss stale pull request approvals when new commits are pushed
10. Building a Quality Culture — Team Practices
Tools enforce floors; culture builds excellence. A team that has internalized quality metrics makes better design decisions before writing code, not just after. The following practices have demonstrable ROI in Java engineering teams:
1. Quality-inclusive Definition of Done: A user story is only "done" when all of the following are true: all acceptance criteria pass, unit test coverage on new code ≥ 80%, zero new SonarQube blocker/critical issues, quality gate is green, and code review approved. Make this explicit in your team's Jira/Linear workflow as a checklist item, not an afterthought.
2. Quality dashboards visible to the whole team: Embed a SonarQube/SonarCloud widget in your team wiki (Confluence, Notion) or display it on a monitor in the engineering area. When debt, coverage, or complexity trends are visible daily, they become team-owned concerns rather than someone else's problem.
3. Regular refactoring sprints: Allocate 10–15% of every sprint capacity to technical debt remediation. Track this as a first-class backlog item type ("tech debt story"), not as spare capacity. Teams that do this consistently maintain debt ratios below 5%. Teams that don't routinely hit D/E ratings within 18 months of a greenfield launch.
4. Code review checklist with quality items: Add explicit quality checkboxes to your PR template:
# .github/pull_request_template.md
## Code Quality Checklist
- [ ] SonarQube quality gate is green
- [ ] New code coverage ≥ 80% (check JaCoCo report)
- [ ] No methods with cyclomatic complexity > 10 introduced
- [ ] No code duplication blocks introduced (> 10 lines)
- [ ] No hardcoded credentials, magic numbers, or TODO left
- [ ] Exception handling: no empty catch blocks
- [ ] All new public methods have meaningful names (no abbreviations)
- [ ] Logging: no sensitive data in log messages5. Measuring quality ROI: Track the correlation between quality metric improvements and business outcomes over quarters. Teams that reduce their SonarQube bug count by 50% typically see a 20–40% reduction in production incident rate within 2 quarters. This data transforms quality investment from a cost centre into a demonstrable competitive advantage in engineering velocity.
- Code Quality: Cyclomatic complexity ≤ 5, coverage ≥ 80%, duplication ≤ 3%.
- JaCoCo: Configure
haltOnFailure=trueto fail builds below threshold — don't just report. - SonarQube: Run locally with Docker, push to SonarCloud in CI. Use
sonar.qualitygate.wait=true. - Quality Gates: Use "Sonar Way" on new code for greenfield and "Sonar Way (legacy)" for inherited codebases.
- Technical Debt: Apply the Boy Scout Rule — always leave code cleaner than you found it.
- CI/CD: Branch protection rules + quality gate enforcement = no merging broken-quality code.
- Culture: Embed quality in DoD, make metrics visible, and allocate sprint capacity to debt remediation.
Leave a Comment
Related Posts
Software Engineer · Java · Spring Boot · Microservices