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.