· 12 Min read

Modern Java Development Stack Setup Guide for 2025

Modern Java Development Stack Setup Guide for 2025

Java development in 2025 looks dramatically different from just a few years ago. The ecosystem has evolved with powerful new frameworks, enhanced tooling, and streamlined development workflows that can boost productivity by 40% or more. This comprehensive guide walks you through setting up a complete modern Java development stack that leverages the most effective tools available today.

The Java landscape in 2025 centers around several key frameworks that have proven their worth in production environments. Spring Boot continues to dominate enterprise development, while Micronaut offers compelling alternatives for microservices. Hibernate remains the go-to ORM solution, and JUnit 5 has revolutionized testing approaches. Understanding how these pieces fit together and setting them up correctly can mean the difference between a productive development experience and constant frustration.

Link to section: Prerequisites and System SetupPrerequisites and System Setup

Before diving into framework-specific configurations, you need a solid foundation. Start by installing Java Development Kit (JDK) 21, which has become the new standard for enterprise applications in 2025. Oracle JDK and OpenJDK both work excellently, but OpenJDK offers the advantage of being completely free for commercial use.

Download OpenJDK 21 from the official website and install it using your system's package manager. On Ubuntu or Debian systems, run these commands:

sudo apt update
sudo apt install openjdk-21-jdk

For macOS users with Homebrew:

brew install openjdk@21

Windows users can download the installer directly or use Chocolatey:

choco install openjdk --version=21.0.1

Verify your installation by checking the version:

java --version
javac --version

You should see output indicating Java 21.0.x. Set your JAVA_HOME environment variable to point to the JDK installation directory. On Linux and macOS, add this to your shell profile:

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH

The specific path varies depending on your installation method and operating system. Use which java to find the correct path if needed.

Link to section: Build Tools: Maven vs Gradle DecisionBuild Tools: Maven vs Gradle Decision

Modern Java development requires a robust build tool, and 2025 presents two excellent options: Maven and Gradle. Maven remains the conservative choice with its XML-based configuration and extensive ecosystem support. Gradle offers more flexibility with its Groovy or Kotlin DSL and faster build times through incremental compilation.

For this guide, we'll use Maven 3.9.x, which includes significant performance improvements and better dependency resolution. Install Maven using your package manager or download it directly from the Apache Maven website.

Ubuntu/Debian installation:

sudo apt install maven

macOS with Homebrew:

brew install maven

Verify the installation:

mvn --version

You should see Maven 3.9.x with Java 21 listed in the output. If you prefer Gradle, the setup process is similar, and most commands in this guide have Gradle equivalents.

Link to section: Spring Boot 3.x Project CreationSpring Boot 3.x Project Creation

Spring Boot 3.x represents a major evolution in the framework, requiring Java 17 as a minimum but performing optimally with Java 21. The framework has embraced native compilation, improved observability, and enhanced security features that make it ideal for modern application development.

Create a new Spring Boot project using Spring Initializr. While you can use the web interface at start.spring.io, the command-line approach provides more control and reproducibility:

curl https://start.spring.io/starter.tgz \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.3.0 \
  -d baseDir=modern-java-app \
  -d groupId=com.example \
  -d artifactId=modern-java-app \
  -d name=modern-java-app \
  -d description="Modern Java Application" \
  -d packageName=com.example.app \
  -d packaging=jar \
  -d javaVersion=21 \
  -d dependencies=web,data-jpa,h2,actuator,security | tar -xzf -

This command creates a complete Spring Boot project structure with essential dependencies. Navigate to the project directory and examine the generated pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>modern-java-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>modern-java-app</name>
    <description>Modern Java Application</description>
    
    <properties>
        <java.version>21</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Test your setup by running the application:

cd modern-java-app
./mvnw spring-boot:run

The application should start on port 8080. You'll see Spring Security's default login page when accessing http://localhost:8080, indicating that all components are working correctly.

Terminal showing Spring Boot application startup logs

Link to section: Configuring Hibernate and Database IntegrationConfiguring Hibernate and Database Integration

Hibernate 6.x brings significant improvements in performance, type safety, and query capabilities. The version included with Spring Boot 3.x provides excellent out-of-the-box configuration, but customizing it for production use requires additional setup.

Create an application configuration file at src/main/resources/application.yml:

spring:
  application:
    name: modern-java-app
  
  datasource:
    url: jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password: password
  
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
        jdbc:
          batch_size: 20
          fetch_size: 100
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.internal.JCacheRegionFactory
  
  h2:
    console:
      enabled: true
      path: /h2-console
  
  security:
    user:
      name: admin
      password: admin123
      roles: ADMIN
 
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

This configuration enables SQL logging, batch processing, and the H2 console for development. Create a simple entity to test the database integration:

package com.example.app.entity;
 
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
 
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Username is required")
    @Column(unique = true)
    private String username;
    
    @Email(message = "Email should be valid")
    @Column(unique = true)
    private String email;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt = LocalDateTime.now();
    
    // Constructors, getters, and setters
    public User() {}
    
    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }
    
    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

Create a repository interface using Spring Data JPA:

package com.example.app.repository;
 
import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
 
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
    
    @Query("SELECT u FROM User u WHERE u.createdAt > :date")
    List<User> findUsersCreatedAfter(LocalDateTime date);
}

Link to section: Adding Micronaut as a Microservices AlternativeAdding Micronaut as a Microservices Alternative

While Spring Boot excels in monolithic applications, Micronaut offers superior performance for microservices architectures. Adding Micronaut support to your development stack provides flexibility when building distributed systems.

Create a separate Micronaut project to understand the differences:

curl -O https://launch.micronaut.io/demo.zip?type=APPLICATION&lang=JAVA&build=MAVEN&test=JUNIT&javaVersion=JDK_21&features=data-jpa,flyway,h2,http-client,jackson-databind,logback,management,micronaut-aot,reactor,security-jwt,validation
 
unzip demo.zip -d micronaut-app
cd micronaut-app

The generated Micronaut project includes similar functionality to Spring Boot but with a different approach to dependency injection and configuration. Key differences include compile-time dependency injection, smaller memory footprint, and faster startup times.

Compare the startup times between Spring Boot and Micronaut by measuring application boot time:

# Spring Boot startup time
time ./mvnw spring-boot:run
 
# Micronaut startup time  
time ./mvnw mn:run

Micronaut typically starts 2-3 times faster than Spring Boot, making it ideal for serverless deployments and microservices that need to scale rapidly.

Link to section: JUnit 5 Testing Setup and Best PracticesJUnit 5 Testing Setup and Best Practices

Testing remains crucial for maintaining code quality, and JUnit 5 provides powerful features for comprehensive test coverage. The Jupiter engine offers parameterized tests, dynamic tests, and improved assertions that make testing more expressive and maintainable.

Create a comprehensive test for the User entity and repository:

package com.example.app.repository;
 
import com.example.app.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
 
import java.time.LocalDateTime;
import java.util.Optional;
 
import static org.junit.jupiter.api.Assertions.*;
 
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    private User testUser;
    
    @BeforeEach
    void setUp() {
        testUser = new User("testuser", "test@example.com");
        entityManager.persistAndFlush(testUser);
    }
    
    @Test
    @DisplayName("Should find user by username")
    void shouldFindUserByUsername() {
        Optional<User> found = userRepository.findByUsername("testuser");
        
        assertTrue(found.isPresent());
        assertEquals("testuser", found.get().getUsername());
        assertEquals("test@example.com", found.get().getEmail());
    }
    
    @Test
    @DisplayName("Should return empty when user not found")
    void shouldReturnEmptyWhenUserNotFound() {
        Optional<User> found = userRepository.findByUsername("nonexistent");
        
        assertTrue(found.isEmpty());
    }
    
    @ParameterizedTest
    @ValueSource(strings = {"user1@test.com", "user2@example.org", "admin@domain.co.uk"})
    @DisplayName("Should handle various email formats")
    void shouldHandleVariousEmailFormats(String email) {
        User user = new User("testuser" + email.hashCode(), email);
        User saved = userRepository.save(user);
        
        assertNotNull(saved.getId());
        assertEquals(email, saved.getEmail());
    }
    
    @Test
    @DisplayName("Should find users created after specific date")
    void shouldFindUsersCreatedAfterDate() {
        LocalDateTime cutoffDate = LocalDateTime.now().minusDays(1);
        
        var users = userRepository.findUsersCreatedAfter(cutoffDate);
        
        assertFalse(users.isEmpty());
        assertTrue(users.stream().allMatch(user -> 
            user.getCreatedAt().isAfter(cutoffDate)));
    }
}

Add integration tests that verify the complete application stack:

package com.example.app.integration;
 
import com.example.app.entity.User;
import com.example.app.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
 
import static org.junit.jupiter.api.Assertions.*;
 
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb-integration",
    "spring.jpa.hibernate.ddl-auto=create-drop"
})
class ApplicationIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldStartApplicationSuccessfully() {
        ResponseEntity<String> response = restTemplate
            .withBasicAuth("admin", "admin123")
            .getForEntity("http://localhost:" + port + "/actuator/health", String.class);
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertTrue(response.getBody().contains("UP"));
    }
    
    @Test
    void shouldPersistDataCorrectly() {
        User user = new User("integrationtest", "integration@test.com");
        User saved = userRepository.save(user);
        
        assertNotNull(saved.getId());
        
        User found = userRepository.findById(saved.getId()).orElse(null);
        assertNotNull(found);
        assertEquals("integrationtest", found.getUsername());
    }
}

Run the complete test suite to verify everything works correctly:

./mvnw test

You should see output indicating all tests pass, with coverage information if you have a coverage plugin configured.

Link to section: Modern IDE Setup and IntegrationModern IDE Setup and Integration

The development environment significantly impacts productivity. In 2025, several IDEs offer excellent Java support with AI-powered coding assistance that can accelerate development workflows. IntelliJ IDEA Ultimate remains the gold standard for Java development, while VS Code with Java extensions provides a lighter alternative.

For IntelliJ IDEA, install the following essential plugins:

  1. Spring Boot Helper - Enhanced Spring Boot support
  2. Hibernate Inspector - Database schema visualization
  3. Maven Helper - Advanced Maven integration
  4. JUnit Generator - Automatic test generation
  5. SonarLint - Code quality analysis

Configure IntelliJ for optimal Java 21 development by setting the Project SDK to Java 21 and enabling preview features. In Settings > Build, Execution, Deployment > Compiler > Java Compiler, set the project bytecode version to 21 and enable "Use '--release' option for cross-compilation."

VS Code users should install the Extension Pack for Java, which includes:

  • Language Support for Java by Red Hat
  • Debugger for Java
  • Test Runner for Java
  • Maven for Java
  • Project Manager for Java

Configure VS Code's settings.json for Java development:

{
    "java.home": "/usr/lib/jvm/java-21-openjdk-amd64",
    "java.configuration.runtimes": [
        {
            "name": "JavaSE-21",
            "path": "/usr/lib/jvm/java-21-openjdk-amd64"
        }
    ],
    "java.compile.nullAnalysis.mode": "automatic",
    "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml",
    "maven.executable.path": "/usr/bin/mvn"
}

Link to section: Docker Integration and ContainerizationDocker Integration and Containerization

Modern Java applications benefit significantly from containerization, enabling consistent deployment across environments. Spring Boot 3.x includes improved Docker integration through buildpacks and native image support.

Create a multi-stage Dockerfile optimized for Java 21 applications:

# Build stage
FROM maven:3.9.6-openjdk-21-slim AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
 
# Runtime stage
FROM openjdk:21-jdk-slim
WORKDIR /app
 
# Create non-root user for security
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --ingroup appgroup appuser
 
# Copy the built JAR from builder stage
COPY --from=builder /app/target/*.jar app.jar
 
# Change ownership of the app directory
RUN chown -R appuser:appgroup /app
USER appuser
 
EXPOSE 8080
 
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1
 
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Build and test the Docker container:

docker build -t modern-java-app:latest .
docker run -p 8080:8080 modern-java-app:latest

For production environments, consider using Spring Boot's buildpack integration, which creates optimized container images:

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=modern-java-app:buildpack

Link to section: Production Configuration and DeploymentProduction Configuration and Deployment

Production deployment requires careful configuration of logging, monitoring, and security settings. Create production-specific configuration in src/main/resources/application-prod.yml:

spring:
  profiles:
    active: prod
  
  datasource:
    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/proddb}
    username: ${DB_USERNAME:produser}
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      max-lifetime: 600000
  
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        jdbc:
          batch_size: 50
          fetch_size: 100
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.internal.JCacheRegionFactory
  
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${JWT_ISSUER_URI}
 
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    export:
      prometheus:
        enabled: true
 
logging:
  level:
    com.example.app: INFO
    org.hibernate.SQL: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: /var/log/app/application.log

Add PostgreSQL dependency to your pom.xml for production database support:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Create a deployment script that handles environment-specific configuration:

#!/bin/bash
# deploy.sh
 
ENV=${1:-staging}
echo "Deploying to $ENV environment..."
 
# Build the application
./mvnw clean package -DskipTests
 
# Build Docker image with environment tag
docker build -t modern-java-app:$ENV .
 
# Deploy based on environment
if [ "$ENV" = "production" ]; then
    docker tag modern-java-app:$ENV registry.company.com/modern-java-app:latest
    docker push registry.company.com/modern-java-app:latest
    
    # Deploy to Kubernetes cluster
    kubectl apply -f k8s/production/
elif [ "$ENV" = "staging" ]; then
    docker run -d -p 8080:8080 \
        -e SPRING_PROFILES_ACTIVE=staging \
        -e DATABASE_URL=$STAGING_DATABASE_URL \
        modern-java-app:staging
fi
 
echo "Deployment to $ENV completed successfully"

Link to section: Monitoring and Observability SetupMonitoring and Observability Setup

Modern applications require comprehensive monitoring to maintain reliability and performance. Spring Boot Actuator provides excellent built-in observability features that integrate with popular monitoring tools.

Add Micrometer dependencies for metrics collection:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>

Create custom metrics to track application-specific data:

package com.example.app.config;
 
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class MetricsConfiguration {
    
    @Bean
    public Counter userRegistrationCounter(MeterRegistry meterRegistry) {
        return Counter.builder("user.registrations")
            .description("Number of user registrations")
            .tag("application", "modern-java-app")
            .register(meterRegistry);
    }
    
    @Bean
    public Counter errorCounter(MeterRegistry meterRegistry) {
        return Counter.builder("application.errors")
            .description("Number of application errors")
            .tag("application", "modern-java-app")
            .register(meterRegistry);
    }
}

This comprehensive setup provides a solid foundation for modern Java development in 2025. The combination of Spring Boot 3.x, Hibernate 6.x, JUnit 5, and proper tooling creates a productive environment capable of handling enterprise-scale applications. The configuration examples, testing strategies, and deployment approaches shown here reflect current best practices and will serve you well as you build robust Java applications.

The key to success lies in understanding how these components work together and customizing the setup for your specific requirements. Whether you're building monolithic applications with Spring Boot or exploring microservices with Micronaut, this foundation provides the flexibility and reliability needed for modern software development.