Java Spring Boot Best Practices

Spring Boot simplifies the development of Spring applications with its convention over configuration approach and many out-of-the-box solutions for common application needs. In this blog post, I explained 15+ important Spring Boot and REST API best practices for developers, each illustrated with "Avoid" and "Better" examples.
Java Spring Boot Best Practices

1. Use Spring Boot Starters

Avoid: Manually configuring Spring dependencies in your pom.xml or build.gradle.

<!-- Example with Maven -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.9</version>
</dependency>
<!-- Additional individual Spring dependencies -->

Better: Utilize Spring Boot starters to handle dependency management.

<!-- Example with Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Explanation: Spring Boot starters include all necessary dependencies related to a specific theme (like web applications), ensuring compatibility and simplifying dependency management.

2. Externalize Configuration

Avoid: Hardcoding configuration properties directly in your code.

@Component
public class Service {
    private String url = "http://example.com/api";
}

Better: Use application.properties or application.yml to externalize configuration.

# application.properties
app.service.url=http://example.com/api
@Component
public class Service {
    @Value("${app.service.url}")
    private String url;
}

Explanation: Externalizing configuration allows for easier changes across different environments and reduces the need to recompile code for configuration changes.

3. Leverage Spring Boot’s Actuator

Avoid: Implementing custom health checks and metrics collection.

public class HealthCheck {
    public String checkHealth() {
        // Custom health check logic
        return "OK";
    }
}

Better: Enable and configure Spring Boot Actuator for health checks and metrics.

management.endpoints.web.exposure.include=health,info,metrics

Explanation: Spring Boot Actuator provides built-in endpoints for monitoring and managing your application, offering features like health status, metrics, and more out of the box.

4. Use Profiles for Environment-Specific Configuration

Avoid: Using the same configuration settings for all environments.

@Bean
public DataSource dataSource() {
    return new DriverManagerDataSource("jdbc:mysql://localhost/prodDb", "user", "pass");
}

Better: Define environment-specific configurations using profiles.

// src/main/resources/application-dev.properties
spring.datasource.url=jdbc:mysql://localhost/devDb

// src/main/resources/application-prod.properties
spring.datasource.url=jdbc:mysql://localhost/prodDb
@Bean
@Profile("dev")
public DataSource devDataSource() {
    // configuration for dev
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
    // configuration for prod
}

Explanation: Using profiles helps manage environment-specific configurations cleanly and reduces the risk of configuration errors between environments.

5. Keep Spring Boot Applications Up to Date

Avoid: Sticking with older versions of Spring Boot without evaluating newer versions.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
</parent>

Better: Regularly update to the latest stable Spring Boot version.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
</parent>

Explanation: Keeping up to date with the latest versions of Spring Boot ensures you benefit from the latest features, improvements, and security patches.

6. Use Spring Boot DevTools for Development Efficiency

Avoid: Manually restarting the server after every change during development.

// Standard Spring Boot setup without DevTools

Better: Include Spring Boot DevTools for automatic restarts and live reload.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
</dependencies>

Explanation: Spring Boot DevTools improves development productivity by automatically restarting the server upon changes and providing additional development-time features.

7. Implement Proper Error Handling

Avoid: Allowing exceptions to propagate to the client without proper handling.

@RestController
public class UserController {
    @GetMapping("/users")
    public List<User> listUsers() {
        // method that might throw unchecked exceptions
    }
}

Better: Use a global exception handler with @ControllerAdvice.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = {Exception.class})
    public ResponseEntity<Object> handleException(Exception ex) {
        return new ResponseEntity<>("An error occurred", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Explanation: Proper error handling centralizes logic in one place, improving maintainability and providing a better client experience by returning cleaner error responses.

8. Optimize Application Startup Performance

Avoid: Ignoring slow startup times without investigating potential causes.

// Application with default configurations and unchecked auto-configuration

Better: Profile and optimize startup performance, possibly using lazy initialization.

spring.main.lazy-initialization=true

Explanation: Optimizing startup time can significantly enhance the development and deployment process, especially in microservices architectures. Lazy initialization delays bean creation until necessary, improving startup time.

9. Secure Your Spring Boot Application

Avoid: Exposing sensitive endpoints without security considerations.

@RestController
public class AdminController {
    @GetMapping("/admin/stats")
    public Stats getStats() {
        // sensitive operation
    }
}

Better: Secure sensitive endpoints using Spring Security.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeHttpRequests((authorize) -> {
                    authorize.requestMatchers("/admin/**").hasRole("ADMIN")
                    authorize.anyRequest().authenticated();
                }).httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

Explanation: Securing your application using Spring Security ensures that only authorized users can access sensitive data or operations, protecting your application against unauthorized access.

10. Test Thoroughly

Avoid: Deploying applications with minimal testing.

public class ProductService {
    public Product findProductById(Long id) {
        // Business logic without corresponding tests
    }
}

Better: Write comprehensive tests using Spring Boot’s testing support.

@SpringBootTest
public class ProductServiceTests {
    @Autowired
    private ProductService productService;

    @Test
    public void testFindProductById() {
        Product product = productService.findProductById(1L);
        assertNotNull(product);
    }
}

Explanation: Thorough testing, including unit tests and integration tests using Spring Boot’s testing features, ensures that your application behaves as expected, reduces bugs, and improves code quality.

11. Use Proper RESTful Resource Naming and HTTP Verbs

Avoid: Inconsistent and non-intuitive API endpoint naming and HTTP method misuse.

@PostMapping("/getProductDetails")
public Product getProductById(@RequestParam("id") Long id) { ... }

Better: Adhere to RESTful naming conventions and appropriate HTTP methods.

@GetMapping("/products/{id}")
public Product getProductById(@PathVariable Long id) { ... }

Explanation: RESTful naming conventions improve the understandability of the API, and using the correct HTTP verbs (GET, POST, PUT, DELETE) aligns with protocol standards, enhancing API usability.

12. Implement Pagination and Filtering

Avoid: Returning large datasets that can lead to performance issues.

@GetMapping("/products")
public List<Product> getAllProducts() { ... }

Better: Use pagination and filtering to manage data delivery.

@GetMapping("/products")
public Page<Product> getProducts(@RequestParam Optional<Integer> page,
                                 @RequestParam Optional<String> filter) {
    return productService.getProducts(page.orElse(0), filter);
}

Explanation: Pagination and filtering reduce the load on the server and improve the client's ability to handle the data, enhancing performance and usability.

13. Use HTTP Status Codes Appropriately

Avoid: Misusing HTTP status codes or not using them effectively to convey the correct semantics.

@PostMapping("/products")
public Product addProduct(@RequestBody Product product) {
    Product savedProduct = productService.save(product);
    return savedProduct; // Always returns 200 OK, even for creation.
}

Better: Return appropriate status codes to reflect the outcome of operations.

@PostMapping("/products")
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
    Product savedProduct = productService.save(product);
    return new ResponseEntity<>(savedProduct, HttpStatus.CREATED); // 201 Created
}

Explanation: Using HTTP status codes correctly (such as 201 Created for POST success) clarifies the result of API calls, making the API more intuitive.

14. Validate Input Data

Avoid: Failing to validate incoming data, leading to potential data integrity issues.

@PostMapping("/products")
public Product addProduct(@RequestBody Product product) {
    return productService.save(product);
}

Better: Apply validation constraints to your request bodies.

@PostMapping("/products")
public ResponseEntity<Object> addProduct(@Valid @RequestBody Product product, BindingResult result) {
    if (result.hasErrors()) {
        return new ResponseEntity<>(result.getAllErrors(), HttpStatus.BAD_REQUEST);
    }
    Product savedProduct = productService.save(product);
    return new ResponseEntity<>(savedProduct, HttpStatus.CREATED);
}

Explanation: Data validation ensures that the incoming data adheres to expected formats and rules, improving the application's reliability and security.

15. Document the API with Swagger or OpenAPI

Avoid: Leaving the API undocumented or poorly documented.

// API methods without documentation
@GetMapping("/products/{id}")
public Product getProductById(@PathVariable Long id) { ... }

Better: Use tools like Swagger to document the API automatically.

@Configuration
public class SwaggerConfig { ... }

// Properly annotated API methods
@GetMapping("/products/{id}")
@ApiOperation(value = "Find product by ID", notes = "Provide an ID to look up specific product")
public Product getProductById(@PathVariable Long id) { ... }

Explanation: Well-documented APIs facilitate easier integration and usage by clearly describing their functionalities, parameters, and responses.

16. Secure the API

Avoid: Exposing APIs without authentication or authorization.

@GetMapping("/products")
public List<Product> getAllProducts() { ... }

Better: Implement security measures such as JWT or OAuth2.

@GetMapping("/products")
@PreAuthorize("hasRole('ADMIN')")
public List<Product> getAllProducts() { ... }

Explanation: Securing APIs ensures that only authorized users can access sensitive data and operations, protecting the application from unauthorized use.

17. Monitor API Performance and Health

Avoid: Ignoring the operational aspects of the API.

// No monitoring or health check implementations.

Better: Implement monitoring and health checks.

@GetMapping("/health")
public ResponseEntity<String> healthCheck() {
    return new ResponseEntity<>("OK", HttpStatus.OK);
}

Explanation: Monitoring and regular health checks help detect and diagnose issues early, ensuring the API's high availability and reliability.

18. Implement Versioning

Avoid: Making breaking changes to the API without version management.

@GetMapping("/products")
public List<Product> getProducts() { ... }

Better: Use API versioning to manage changes gracefully.

@GetMapping("/api/v1/products")
public List<Product> getProductsV1() { ... }

@GetMapping("/api/v2/products")
public List<Product> getProductsV2() { ... }

Explanation: Versioning allows you to make backwards-incompatible changes without affecting existing clients, facilitating smoother transitions and clear communication of changes.

Comments