Spring is an application framework and inversion of control container for the Java platform. These tips are based on Spring documentation, books, articles and professional experience.
- Follow code conventions
- Follow a package naming convention
- Use Maven/Gradle wrapper
- Use Spring Cloud
- Use Spring Boot starters
- Use OpenAPI and Swagger UI
- Use code generators
- Use database migrations
- Use controllers only for routing
- Use services for business logic
- Use repository pattern
- Use validators
- Use DTOs
- Use caching
- Provide a global exception handling
- Avoid global state and mutability
- Remove unused code
- Set logging levels
- Expose health checks and metrics
- Externalize all configurations
- Analyse your code
- Check dependencies for vulnerabilities
- Compile natively
- Prefer Spring WebFlux
- Test your code
Code conventions are base rules that allow the creation of a uniform code base across an organization. Following them does not only increase the uniformity and therefore the quality of the code. Oracle Code Conventions and Google Java Style Guide are the two main coding styles for Java. Checkstyle is a tool to help programmers find class design problems, method design problems, and others. It also can check code layout and formatting issues. Prettier combined with Spotless can be used to enforce a consistent code style. Code conventions must be dynamic and adaptable for each team and project.
Proper packaging will help to understand the code and the flow of the application easily. You can structure your application with meaningful packaging. You can use the following naming convention for your packages:
- The
entity
package contains the database entities of the application. - The
repository
package contains all the repositories-related classes. - The
service
package contains all the business logic-related classes. - The
controller
package contains all controllers classes of the application. - Other common packages are
config
,mapper
,filter
,exception
, etc.
This style is very convenient in small-size microservices. If you are working on a huge code base, a feature-based approach can be used.
The recommended way to execute any Maven/Gradle build is with the help of the wrapper (Maven Wrapper and Gradle Wrapper). Instead of installing many versions of it in the operating system, you can just use the project-specific wrapper script. The wrapper is a script that invokes a declared version of Maven/Gradle, downloading it beforehand if necessary. As a result, developers can get up and running with a Maven/Gradle project quickly without having to follow manual installation processes saving time.
Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand-up services and applications that implement those patterns. They will work well in any distributed environment, including the developer's own laptop, bare metal data centres, and managed platforms.
Dependency management is a critical aspect of any complex project. Spring Boot provides several starters that allow you to add JARs in the classpath.
In the Spring Boot, all the starters follow a similar naming pattern: spring-boot-starter-*
, where *
denotes a particular type of starter.
You can use Spring Initializr to help you create a new Spring Boot project and choose possible dependencies according to your needs.
Furthermore, you can add spring-boot-devtools dependency to take advantage of development-time features provided by the Developer Tools, such as automatic restart, LiveReload and global Settings.
OpenAPI Specification is the factual standard for creating REST APIs. An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases. Swagger UI allows anyone to visualize and interact with the API's resources without having any of the implementation logic in place. It's automatically generated from your OpenAPI Specification. You just need to add springdoc-openapi-ui dependency to your project to automate the generation of API documentation.
Java is a great language, but it can sometimes get too verbose for common tasks. Lombok is a Java library that is used to minimize or remove the boilerplate code. Just by using the annotations, you can save space and readability of the source code. MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. Also, OpenAPI Specification can be used by code generation tools to generate servers and clients for Java and Spring applications. For that, you can use OpenAPI Generator plugin for Maven/Gradle.
Database migrations is a process of making changes to database schema during a development process. You wouldn't develop app code without version control. The same should be true for database changes. Migrations are most commonly written in SQL. Spring Boot supports Flyway and Liquibase migration tools. If you are using Liquibase, you will have defined your database as a set of Liquibase change sets, in XML, YAML, or JSON , it can be easily used as a source of meta information by the jOOQ code generator.
Controllers are dedicated to routing. Controllers are the ultimate target of requests, then requests will be handed over to the service layer and processed by the service layer. They are stateless and all business logic should not place on them. Controllers should deal with the HTTP layer of the application and oriented around a business capability. See the code snippet of a controller:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> findAll() {
return userService.findAll();
}
}
The business logic of the application goes here with validations, caching, etc. Build your services around business capabilities/domains/use-cases. Services communicate with the persistence layer and receive the results. Services are singleton and annotated with @Service
. See the code snippet of a service:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> findAll() {
return userRepository.findAll();
}
}
Repositories are a very popular pattern for Java-based persistence layers. They encapsulate the database operations you can perform on entity objects and aggregates. That helps to separate the business logic from the database operations and improves the reusability of your code. Spring Data JPA provides repository support for the Jakarta Persistence API (JPA). See the code snippet of a repository:
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {}
REST APIs need to validate the data it receives, and Spring provides rich built-in support for validating REST API request objects.
Spring Boot Starter Validation offers standard validation primitives such as @NotNull
, @NotBlank
, @Size
, or @Email
and if you need anything more advanced, you can easily implement your custom annotations.
See the code snippet of a DTO:
public class User {
@NotNull
@Size(min = 4, max = 20)
public String username;
@NotNull
@Size(min = 8, max = 20)
public String password;
}
While it is also possible to directly expose the database entities on REST endpoints to send/receive client data, this is not the best approach. It creates high coupling between the persistence models and the API models. The better approach is defining a separate Data Transfer Object (DTO) that represents the API resource class which is mapped from a database entity or multiple entities. To do this mapping, you can use MapStruct. With DTOs, you can build different views from your domain models, allowing you to create other representations of the same domain but optimizing them to the clients' needs without affecting your domain design.
Caching is an important factor when talking about application performance.
If you use Spring Boot, then you can utilize the spring-boot-starter-cache dependency to easily add the caching dependencies to your project.
You can enable the caching feature simply by adding the @EnableCaching
annotation to any of the configuration classes.
Once you have enabled caching, the next step is to bind the caching behaviour to the methods with declarative annotations.
When caching is enabled, then the application first looks for required object in the cache instead of fetching it from the source.
If you are not satisfied with default caching, you can use Hazelcast, Redis, or any other distributed caching implementations.
Besides the classic 404 error page, you should also look at what our application returns in case of an uncaught exception.
Normally, exceptions will be translated to a 500 error (Internet Server Error) and written to the log.
@ControllerAdvice
is an annotation provided by Spring allowing you to write global code that can be applied to your controllers.
See the code snippet of a Global Exception Handler:
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(IOException.class)
protected ResponseEntity<Object> handleIOException(RuntimeException ex, WebRequest request) {
return handleExceptionInternal(ex, "IOException handler executed", new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
}
Also, you should set server.error.include-stacktrace=never
for production environments to avoid including stack trace in the server error.
Problems caused by parallel execution of programs are nerve-wrackingly elusive and often times extremely difficult to debug. First, always remember the "global state" issue. If you're creating a multithreaded application, absolutely anything that is globally modifiable should be closely monitored and, if possible, removed altogether. Immutability comes directly from functional programming and, adapted to OOP, states that class mutability and changing state should be avoided. This, in short, means foregoing setter methods and having private final fields on all your model classes. This way you can be certain that no contention problems arise and that accessing object properties will provide the correct values at all times.
Unused code or dead code is any code which will never be executed. It may be some condition, loop or any file which was simply created but wasn't used in our project. It is a problem because that code has no sense, and you can drop it. Less code also increases maintenance, IDE performance and makes it easier to understand. The quickest way to find dead code is to use a good IDE. You can delete unused code and unneeded files. Also, you can delete unnecessary classes and parameters. Tools like OpenRewrite or Spotless to remove unused code.
Logs are supposed to be a consistent and reliable source of information, which makes troubleshooting systems easier.
Sometimes the logs provide too much information and other times they do not provide enough data.
To set the logging level in a Spring Boot application, you can change the logging settings in the application.properties file.
All the supported logging systems can have logger levels configured using logging.level.<logger-name>=<level>
, where the level is one of ERROR, WARN, INFO, DEBUG, or TRACE, or OFF.
By default, ERROR, WARN, and INFO level messages are logged.
In production environments you should avoid DEBUG or TRACE levels.
Spring Boot supports you with readiness/liveness health checks via Spring Boot Actuator. It allows applications to provide information about their state to external viewers which is typically useful in cloud environments where automated processes must be able to determine whether the application should be discarded or restarted. Depending on the HTTP status code returned from a GET request, the agent will act when it receives an "unhealthy" response. Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics facade that supports numerous monitoring systems. These metrics can be read remotely to be processed by additional tools such as Prometheus and stored for analysis and visualization.
Spring Boot allows you to externalize your configuration so you can work with the same application code in different environments.
Spring Boot reads configuration properties from system properties, environment variables, and application.properties in descending ordinal. Spring Boot supports different properties based on the Spring active profile.
You can define profile-specific files like application-{profile}.properties
.
Also, you can use Spring Cloud Config to externalize application configuration files at runtime and have a central place to manage these properties across all environments.
To ensure long-term code maintainability, you should follow best coding practices and style guide rules. A linter is a static code analysis tool used to flag programming errors, bugs, stylistic errors, and suspicious constructs. Checkstyle is a tool to help programmers find class design problems, method design problems, and others. PMD finds common programming flaws like unused variables, empty catch blocks, unnecessary object creation, and so forth. SpotBugs is used to perform static analysis on Java code. It looks for instances of "bug patterns". You can combine these tools to achieve better results. These checks can also be done by SonarQube.
It is important to ensure that there are no known vulnerabilities throughout your application's dependency tree. Therefore, you should frequently update your application's dependencies to the latest versions. You can use OWASP Dependency-Check to detect publicly disclosed vulnerabilities contained within a project's dependencies. To implement it, just add dependency-check-maven in your pom.xml file or org.owasp.dependencycheck in your build.gradle file. When you choose a base image for your project, you indirectly assume the risk of all the container security concerns that the base image is bundled with. Trivy is a simple and comprehensive vulnerability scanner for Docker images and other artifacts.
Native images provide key advantages, such as instant startup, instant peak performance, and reduced memory consumption. The native executable for your application will contain the application code, required libraries, Java APIs, and a reduced version of a VM. To package a Spring Boot application into a native executable, you need to use Spring Native and Native Build Tools. Native Build Tools are plugins shipped by GraalVM for both Maven and Gradle. When writing native image applications, it is recommended that you continue to use the JVM whenever possible to develop the majority of your unit and integration tests. This will help keep developer build times down and allow you to use existing IDE integrations.
Reactive programming is a programming paradigm where the focus is on developing asynchronous and non-blocking applications. One of the main reasons why developers should switch from blocking to non-blocking code is efficiency. Spring 5 introduced Spring WebFlux to support the reactive web in a non-blocking manner. Spring WebFlux is based on the Reactor API, just another awesome implementation of the reactive stream. Spring WebFlux supports annotation-based configurations in the same way as the Spring Web MVC framework. You just need to add spring-boot-starter-webflux dependency to your project to start coding.
If you have no tests or an inadequate amount, then every time you ship code, you won't be sure that you didn't break anything.
Always write tests for every new feature/module you introduce.
Spring Boot provides the @SpringBootTest
annotation which you can use to create an application context containing all the objects you need for all test types.
Most developers will just use spring-boot-starter-test which imports both Spring Boot test modules as well has JUnit, Mockito, AssertJ and several other useful libraries.
If you've written tests with Spring or Spring Boot in the past, you'll probably notice that you don't need Spring to write unit tests.
You just need to use JUnit and Mockito.
It is also recommended that you keep integration tests separate from unit tests and not run them alongside unit tests.
- 16 Best Practices for Spring Boot in Production
- A Guide to Caching in Spring
- Best Practices for How to Test Spring Boot Applications
- Best Practices for Structuring Spring Boot Application
- Database Migrations with Flyway
- Documenting a Spring REST API Using OpenAPI 3.0
- Error Handling for REST with Spring
- Guide to Spring 5 WebFlux
- Introduction to Spring Data JPA
- Java Bean Validation
- Logging in Spring Boot
- Properties with Spring and Spring Boot
- Spring Boot Microservices Coding Style Guidelines and Best Practices
- Spring Boot Starters
- Spring Boot Tips, Tricks and Techniques
- Spring Profiles
- Testing in Spring Boot
- Tips, Tricks, and Springs: Best Spring Practices
- Top 10 Spring Security best practices for Java developers
- Top Spring Framework Mistakes
- Use Liquibase to Safely Evolve Your Database Schema