2.6 Best Practices

This section synthesizes the entire Chapter 2 journey—modularity, distribution, and build tools—into concrete practices that separate prototype code from production-grade applications.

Principle 1: Reproducible Builds

A reproducible build produces bit-for-bit identical artifacts when built on different machines or at different times. This is critical for security verification, compliance, and debugging.

Why It Matters

  • Security: Anyone can verify that released binaries match published source code
  • Compliance: Auditors can trace exactly what went into production
  • Debugging: If a bug appears in production, you can rebuild that exact version and reproduce it locally

Implementing Reproducible Builds

Maven Reproducible Builds Plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <source>25</source>
        <target>25</target>
        <!-- Reproducible compilation: strip timestamps, use deterministic ordering -->
        <enableAssertions>true</enableAssertions>
        <fork>true</fork>
    </configuration>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
        <!-- Ensure JAR entries are ordered, timestamps are stripped -->
        <archive>
            <reproducible>true</reproducible>
        </archive>
    </configuration>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.5.0</version>
    <configuration>
        <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.example.Application</mainClass>
            </transformer>
        </transformers>
        <!-- Reproducible FAT JAR -->
        <shadedArtifactAttached>true</shadedArtifactAttached>
    </configuration>
</plugin>

Gradle (Kotlin DSL):

plugins {
    id("java")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

tasks.jar {
    isReproducibleFileOrder = true
    isPreserveFileTimestamps = false
    archiveBaseName.set("myapp")
    manifest {
        attributes("Main-Class" to "com.example.Application")
    }
}

Check reproducibility:

# Build twice and compare checksums
mvn clean package
sha256sum target/myapp-1.0.0.jar > build1.sha

rm -rf target
mvn clean package
sha256sum target/myapp-1.0.0.jar > build2.sha

diff build1.sha build2.sha  # Should be identical

Principle 2: Semantic Versioning

Use Semantic Versioning (SemVer) for predictable version numbers that communicate intent to users.

SemVer Format: MAJOR.MINOR.PATCH

  • MAJOR: Incompatible API changes (breaking changes)
  • MINOR: Backwards-compatible functionality added
  • PATCH: Backwards-compatible bug fixes

Examples:

1.0.0     → 1.0.1     Bug fix
1.0.0     → 1.1.0     New feature (backwards compatible)
1.0.0     → 2.0.0     Breaking change (incompatible API)

2.5.3     → 2.6.0     New feature in 2.x
2.5.3     → 3.0.0     Major redesign (breaking changes)

Declaring versions in Maven:

<groupId>com.example</groupId>
<artifactId>mylib</artifactId>
<version>2.5.3</version>  <!-- MAJOR.MINOR.PATCH -->

Declaring versions in Gradle:

version = "2.5.3"

Pre-release and Build Metadata

2.0.0-alpha.1           Pre-release version (alpha)
2.0.0-beta+build.123    Pre-release with build metadata
2.0.0-rc.1              Release candidate

Maven: 2.0.0-alpha
Gradle: 2.0.0-alpha

Principle 3: Code Signing and Notarization

For native installers and published artifacts, signing ensures authenticity and prevents tampering.

Code Signing: macOS .pkg

Create signing certificate:

# Use your Apple Developer account
# Download certificate from developer.apple.com
# Install in Keychain
security import developer-cert.p12 -k ~/Library/Keychains/login.keychain

Sign jpackage installer:

jpackage \
    --name MyApp \
    --input dist \
    --main-jar MyApp.jar \
    --main-class com.example.Application \
    --type pkg \
    --mac-sign \
    --mac-signing-key-user-name "Developer Name" \
    --mac-package-identifier com.example.myapp

Notarize (required for Gatekeeper on macOS 10.15+):

# Submit to Apple for notarization
xcrun notarytool submit MyApp-1.0.0.pkg \
    --apple-id "your@email.com" \
    --password "app-specific-password" \
    --team-id "ABCD123456"

# Check status
xcrun notarytool info REQUEST-UUID \
    --apple-id "your@email.com" \
    --password "app-specific-password"

Code Signing: Linux .deb

Create GPG key for signing:

gpg --gen-key

Sign .deb package:

dpkg-sig --sign builder -k KEY-ID MyApp-1.0.0.deb

Code Signing: Windows .msi

Create code signing certificate:

New-SelfSignedCertificate `
    -Type CodeSigningCert `
    -Subject "CN=MyApp Code Signing" `
    -CertStoreLocation Cert:\CurrentUser\My

Sign with jpackage:

jpackage ^
    --name MyApp ^
    --input dist ^
    --main-jar MyApp.jar ^
    --main-class com.example.Application ^
    --type msi ^
    --win-dir-chooser ^
    --win-menu ^
    --win-per-user-install

Principle 4: Continuous Integration/Deployment Pipeline

Automate testing, building, signing, and publishing to prevent manual errors.

Complete GitHub Actions Workflow

name: Release

on:
  push:
    tags:
      - 'v*'  # Trigger on version tags (v1.0.0, etc.)

env:
  JAVA_VERSION: '25'
  MAVEN_VERSION: '3.9.11'

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: maven

      - name: Run tests
        run: mvn test

      - name: Build JAR
        run: mvn package -DskipTests

      - name: Build runtime image (jlink)
        run: |
          mvn clean
          mvn package
          jlink \
            --module-path target/classes \
            --add-modules com.example.myapp \
            --output dist/runtime \
            --strip-debug \
            --compress=2

      - name: Package JAR for distribution
        run: |
          mkdir -p dist/lib
          cp target/myapp-*.jar dist/lib/

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            dist/lib/myapp-*.jar
            dist/runtime/**
          draft: false
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Publish to Maven Central
        run: |
          mvn deploy \
            -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} \
            -Dgpg.secretKeyring=${{ secrets.GPG_SECRET_KEYRING }}

Local Release Checklist

#!/bin/bash
set -e

echo "=== Pre-release Checklist ==="
echo "[*] Running all tests..."
mvn clean test

echo "[*] Checking for uncommitted changes..."
git diff --exit-code || (echo "Commit changes first"; exit 1)

echo "[*] Scanning for vulnerabilities..."
mvn org.owasp:dependency-check-maven:check

echo "[*] Building release artifacts..."
mvn clean package -DskipTests

echo "[*] Checking artifact reproducibility..."
sha256sum target/myapp-*.jar > build1.sha
rm -rf target
mvn clean package -DskipTests
sha256sum target/myapp-*.jar > build2.sha
diff build1.sha build2.sha || (echo "Build not reproducible!"; exit 1)

echo "[*] Verifying modular structure..."
jar --describe-module --file=target/myapp-*.jar

echo "[*] All checks passed. Ready to release."

Principle 5: Environment-Specific Builds

Different environments (dev, staging, production) need different configurations.

Maven Profiles

<profiles>
    <!-- Development: verbose logging, local database -->
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <log.level>DEBUG</log.level>
            <db.url>jdbc:mariadb://localhost:3306/myapp_dev</db.url>
            <db.user>dev_user</db.user>
        </properties>
    </profile>

    <!-- Staging: moderate logging, staging database -->
    <profile>
        <id>staging</id>
        <properties>
            <log.level>INFO</log.level>
            <db.url>jdbc:mariadb://staging-db:3306/myapp</db.url>
            <db.user>staging_user</db.user>
        </properties>
    </profile>

    <!-- Production: minimal logging, encrypted secrets -->
    <profile>
        <id>prod</id>
        <properties>
            <log.level>WARN</log.level>
            <db.url>jdbc:mariadb://prod-db.example.com:3306/myapp</db.url>
            <!-- Secrets from GitHub Secrets or CI/CD platform -->
        </properties>
    </profile>
</profiles>

Build for specific environment:

mvn clean package -P prod

Gradle Build Variants (Kotlin DSL)

plugins {
    id("java")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

// Define build variants
project.ext["environment"] = System.getenv("BUILD_ENV") ?: "dev"

tasks.processResources {
    filesMatching("application.properties") {
        expand(project.properties)
    }
}

val dev by configurations.registering
val staging by configurations.registering
val prod by configurations.registering

tasks.register("buildDev") {
    doLast {
        println("Building for development...")
        tasks.build.get().actions.forEach { it.execute(null) }
    }
}

tasks.register("buildProd") {
    doLast {
        println("Building for production...")
        project.ext["environment"] = "prod"
        tasks.build.get().actions.forEach { it.execute(null) }
    }
}

Build:

gradle buildDev   # Development build
gradle buildProd  # Production build

Principle 6: Performance Optimization

Incremental Compilation

Enable incremental builds to compile only changed files:

Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <source>25</source>
        <target>25</target>
        <!-- Incremental compilation -->
        <useIncrementalCompilation>true</useIncrementalCompilation>
    </configuration>
</plugin>

Gradle (default): Gradle supports incremental compilation by default. Check build times:

gradle build --profile

Parallel Compilation

Maven:

mvn -T 1C clean package  # -T 1C = 1 thread per core

Gradle:

org.gradle.parallel=true
org.gradle.workers.max=8  # in gradle.properties

Dependency Cache Management

Maven clean cache (nuclear option):

rm -rf ~/.m2/repository
mvn clean install

Gradle clean cache:

gradle cleanBuildCache
gradle build

Principle 7: Security Hardening

Minimize Attack Surface

Least Privilege Principle:

<!-- Only include necessary dependencies -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.13.0</version>
    <scope>compile</scope>
</dependency>

<!-- Don't include unused libraries -->
<!-- Bad: <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
</dependency> -->

Regular Security Audits

In CI/CD:

mvn org.owasp:dependency-check-maven:check

SBOM (Software Bill of Materials):

mvn org.cyclonedx:cyclonedx-maven-plugin:generateSbom

Sign Artifacts

All released artifacts should be signed and verifiable:

mvn clean deploy -Dgpg.executable=gpg -Dgpg.keyname=YOUR_KEY_ID

Principle 8: Documentation

Keep CHANGELOG.md

# Changelog

#### [2.1.0] - 2024-01-15
#### Added
- Virtual threads support for async operations (#42)
- Pattern matching enhancements (#38)

#### Changed
- Updated Spring Boot to 3.2.0 (#35)
- Improved database connection pooling (#39)

#### Fixed
- Memory leak in request handler (#41)
- NPE in date formatting (#40)

#### Security
- Updated commons-text to 1.11.0 (CVE-2023-50270) (#43)

#### [2.0.0] - 2024-01-01
#### Changed
- **BREAKING**: Migrated to modular architecture (Java modules)
- **BREAKING**: Updated Maven from 3.8.x to 3.9.x

#### Added
- Custom runtime image with jlink
- Native installers with jpackage

Document Dependencies

/**
 * Configuration for database access.
 *
 * Dependencies:
 * - mariadb-java-client: JDBC driver (runtime scope)
 *   Provides: java.sql integration for MariaDB
 *   Reason: Production databases use MariaDB; PostgreSQL alternative via Maven profile
 *
 * - HikariCP: Connection pooling (compile scope)
 *   Provides: com.zaxxer.hikari for optimal performance
 *   Reason: 10x faster than default pooling with minimal memory overhead
 *
 * Configuration:
 * - See application.properties for datasource settings
 * - See DatabaseConfig.java for pooling parameters
 */
public class DatabaseModule {
    public static void main(String[] args) {
        // Implementation
    }
}

Complete Release Process Example

#!/bin/bash
set -e

VERSION=$1
if [ -z "$VERSION" ]; then
    echo "Usage: ./release.sh 2.1.0"
    exit 1
fi

echo "=== Releasing version $VERSION ==="

# 1. Verify preconditions
echo "[1/8] Checking preconditions..."
mvn clean test org.owasp:dependency-check-maven:check

# 2. Update version
echo "[2/8] Updating version to $VERSION..."
mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false

# 3. Build release artifacts
echo "[3/8] Building release artifacts..."
mvn clean package -DskipTests

# 4. Create jlink runtime
echo "[4/8] Creating custom runtime..."
jlink \
    --module-path target/classes \
    --add-modules com.example.myapp \
    --output dist/runtime-$VERSION \
    --compress=2 \
    --strip-debug

# 5. Commit version change
echo "[5/8] Committing version..."
git add pom.xml
git commit -m "Release version $VERSION"

# 6. Create git tag
echo "[6/8] Creating git tag..."
git tag -a v$VERSION -m "Release version $VERSION"

# 7. Deploy to Maven Central
echo "[7/8] Deploying to Maven Central..."
mvn deploy -DskipTests -Dgpg.executable=gpg

# 8. Push to repository
echo "[8/8] Pushing to repository..."
git push origin main
git push origin v$VERSION

echo "=== Release $VERSION complete ==="
echo "Released artifacts available at:"
echo "  - JAR: target/myapp-$VERSION.jar"
echo "  - Runtime: dist/runtime-$VERSION/"
echo "  - Maven Central: https://mvnrepository.com/artifact/com.example/myapp/$VERSION"

Conclusion

Production-grade Java applications aren't built by accident. They result from:

  • Reproducible builds that ensure consistency and security
  • Clear versioning that communicates intent
  • Code signing that verifies authenticity
  • Automated pipelines that prevent human error
  • Security scanning that catches vulnerabilities early
  • Environment-specific configs that prevent misdeployment
  • Performance optimization that respects developer time
  • Documentation that enables maintainability

Apply these practices systematically, and your applications will be reliable, secure, and maintainable for years to come.