SonarQube for Spring Boot: A Practical Setup and Code Quality Guide

Most teams I’ve joined have SonarQube installed somewhere. Very few actually use it. The dashboard exists, the numbers are red, and everyone agrees they’ll “fix it later.” Later never comes.

On every project I’ve worked on — banking compliance, logistics platforms, healthcare systems — I’ve made SonarQube a non-negotiable part of the daily workflow. Not as a reporting tool that generates guilt, but as a quality gate that blocks bad code from reaching production.

This guide covers the practical setup I use, the rules worth enforcing, and how to make code quality something the team follows without being forced.

Why SonarQube Matters More Than You Think

Static analysis isn’t about making code “perfect.” It’s about catching the problems that humans consistently miss during code reviews:

  • Security vulnerabilities that Checkmarx and OWASP scanners flag in audits — SQL injection, hardcoded credentials, insecure deserialization.
  • Null pointer bugs hiding in edge cases nobody tested.
  • Resource leaks — unclosed streams, connections, and file handles that cause production memory issues weeks after deployment.
  • Code duplication that silently multiplies every bug fix by 3.

In a banking KYC system, a single SonarQube scan caught a hardcoded database password in a test utility class that had been committed 8 months earlier. Nobody noticed during 8 months of code reviews. The scanner did.

Step 1: Add the SonarQube Maven Plugin

Add the plugin to your pom.xml:

<properties>
    <sonar.host.url>http://your-sonarqube-server:9000</sonar.host.url>
    <sonar.projectKey>your-project-key</sonar.projectKey>
    <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
    <sonar.coverage.jacoco.xmlReportPaths>
        ${project.build.directory}/site/jacoco/jacoco.xml
    </sonar.coverage.jacoco.xmlReportPaths>
</properties>

<build>
    <plugins>
        <!-- JaCoCo for test coverage reporting -->
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.11</version>
            <executions>
                <execution>
                    <goals><goal>prepare-agent</goal></goals>
                </execution>
                <execution>
                    <id>report</id>
                    <phase>test</phase>
                    <goals><goal>report</goal></goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Run the analysis:

mvn clean verify sonar:sonar \
  -Dsonar.token=your-sonar-token

That’s the entire local setup. JaCoCo generates the coverage report, and the SonarQube plugin uploads everything — code smells, bugs, vulnerabilities, and coverage data.

Step 2: Configure a Quality Gate That Actually Blocks

SonarQube’s default quality gate is too lenient. Here’s the gate I configure on every project:

Metric Threshold Why
New code coverage ≥ 80% New code must be tested. Legacy code gets a pass (for now).
New code duplications ≤ 3% Duplication is the root cause of most maintenance nightmares.
New bugs 0 Zero tolerance for static-analysis-detectable bugs on new code.
New vulnerabilities 0 Zero tolerance for security issues on new code.
New security hotspots reviewed 100% Every hotspot must be explicitly reviewed and marked safe or fixed.

The key phrase is “new code.” This is what makes SonarQube practical on legacy codebases. You don’t need to fix 500 existing issues before the gate passes. You just need to stop adding new ones.

Over 3–6 months, as you touch existing files, the overall numbers improve naturally.

Step 3: Jenkins Pipeline Integration

In your Jenkinsfile, add the SonarQube analysis as a mandatory stage:

pipeline {
    agent any

    stages {
        stage('Build & Test') {
            steps {
                sh 'mvn clean verify'
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube-Server') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                // Only reached if quality gate passes
                sh './deploy.sh'
            }
        }
    }
}

The critical line is abortPipeline: true. If the quality gate fails, the build fails. No exceptions. No “we’ll fix it next sprint.”

This is the single most impactful change I make on any team. The moment bad code can’t reach production, code quality improves dramatically — not because of the tool, but because of the feedback loop.

The 5 Code Smells Worth Fixing First

SonarQube will flag hundreds of issues on a legacy codebase. Don’t try to fix them all. Focus on these five — they cause the most production incidents:

1. Resource Leaks (Critical)

// BAD — connection never closed if an exception occurs
public User getUser(Long id) {
    Connection conn = dataSource.getConnection();
    PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
    stmt.setLong(1, id);
    ResultSet rs = stmt.executeQuery();
    // If anything throws here, connection leaks
    return mapUser(rs);
}

// GOOD — try-with-resources guarantees cleanup
public User getUser(Long id) {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
        stmt.setLong(1, id);
        try (ResultSet rs = stmt.executeQuery()) {
            return mapUser(rs);
        }
    }
}

2. Catching Generic Exceptions

// BAD — swallows everything, including OutOfMemoryError
try {
    processPayment(order);
} catch (Exception e) {
    log.error("Error", e);
}

// GOOD — catch specific exceptions, handle or rethrow
try {
    processPayment(order);
} catch (PaymentDeclinedException e) {
    log.warn("Payment declined for order {}: {}", order.getId(), e.getMessage());
    throw new BusinessException("PAYMENT_DECLINED", e.getMessage(), 422);
} catch (PaymentGatewayTimeoutException e) {
    log.error("Payment gateway timeout for order {}", order.getId(), e);
    throw new ServiceUnavailableException("Payment gateway unavailable");
}

3. Hardcoded Credentials (Security)

// BAD — SonarQube flags this as a blocker
private static final String DB_PASSWORD = "pr0duction_p@ss!";

// GOOD — externalize to environment or vault
@Value("${spring.datasource.password}")
private String dbPassword;

4. Mutable Shared State

// BAD — SimpleDateFormat is not thread-safe
private static final SimpleDateFormat formatter =
    new SimpleDateFormat("yyyy-MM-dd");

// GOOD — DateTimeFormatter is immutable and thread-safe
private static final DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("yyyy-MM-dd");

5. Empty Catch Blocks

// BAD — silent failure, impossible to debug in production
try {
    sendNotification(user);
} catch (NotificationException e) {
    // TODO: handle this
}

// GOOD — at minimum, log it
try {
    sendNotification(user);
} catch (NotificationException e) {
    log.warn("Failed to send notification to user {}: {}",
             user.getId(), e.getMessage());
}

Making It Stick: The Cultural Part

Tools don’t change behavior. Feedback loops do. Here’s what actually works:

  1. Make the quality gate visible. Put the SonarQube dashboard on a screen in the team area (or pin it in Slack). Green/red is a powerful motivator.
  2. Review hotspots together. When SonarQube flags a security hotspot, review it as a team. It’s a learning opportunity, not a blame exercise.
  3. Celebrate improvements. When overall coverage goes from 35% to 55% over a quarter, acknowledge it. It’s real progress.
  4. Lead by example. If the senior engineer’s PRs pass the quality gate clean, everyone else will follow.

The Bottom Line

SonarQube isn’t a magic tool that makes code better. It’s a feedback loop that makes bad code visible before it reaches production. Combined with a quality gate in your CI/CD pipeline, it turns code quality from a conversation topic into an automated standard.

The setup takes an afternoon. The impact lasts for years.