Spring MVC Best Practices

with Code Examples

Bubu Tripathy
4 min readMar 15, 2023

Introduction

Spring MVC is a widely used Java-based framework for building web applications. It offers a robust and flexible architecture that promotes good coding practices and a clean separation of concerns. In this blog post, we will discuss some best practices for Spring MVC and provide code examples to help you implement them in your projects.

Use a Standard Project Structure

Maintaining a standard project structure helps in keeping your code organized and easier to understand. A typical Spring MVC project structure should include the following folders:

  • src/main/java: For Java source files
  • src/main/resources: For resource files, such as application.properties or application.yml
  • src/main/webapp: For web resources like JSPs, JavaScript files, and stylesheets

Use Annotations for Configuration

Spring MVC supports both XML and annotation-based configurations. However, using annotations is the preferred approach, as it makes your code more readable and easier to maintain.

Example:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.example.springmvc"})
public class WebConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
}

Implement a Centralized Exception Handling Mechanism

Instead of handling exceptions in individual controllers, implement a centralized exception handling mechanism using @ControllerAdvice and @ExceptionHandler annotations.

Example:

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception ex) {
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("exception", ex);
return modelAndView;
}
}

Use @RestController for REST APIs

For building RESTful APIs, use the @RestController annotation instead of the traditional @Controller. This helps in automatically converting response objects into JSON or XML format, making it easier to work with API endpoints.

Example:

@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable("id") Long id) {
User user = userService.findUserById(id);
return new ResponseEntity<>(user, HttpStatus.OK);
}
}

Validate User Input

Validate user input using the @Valid annotation and BindingResult object to ensure data integrity and prevent security vulnerabilities.

Example:

@PostMapping("/create")
public String createUser(@ModelAttribute("user") @Valid User user, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "create-user";
}
userService.saveUser(user);
return "redirect:/users";
}

Use @Autowired for Dependency Injection

Leverage Spring’s built-in dependency injection mechanism by using the @Autowired annotation to inject dependencies into your components.

Example:

@Controller
public class UserController {

@Autowired
private UserService userService;

// Controller methods ...
}

Use Asynchronous Processing

For long-running tasks or tasks that do not require an immediate response, use asynchronous processing to improve the application’s performance and responsiveness. Use the @Async annotation along with a task executor to execute tasks asynchronously.

Example:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("AsyncTask-");
executor.initialize();
return executor;
}
}
@Service
public class AsyncService {

@Async
public CompletableFuture<String> asyncTask() {
// Perform a long-running task
return CompletableFuture.completedFuture("Task result");
}
}

Leverage Spring Security for Authentication and Authorization

Integrate Spring Security into your application to handle authentication and authorization. This will ensure that only authorized users can access specific parts of your application.

Example:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().permitAll();
}
}

Implement Pagination and Sorting

To improve performance and user experience, use pagination and sorting when fetching large datasets. Spring Data JPA provides built-in support for pagination and sorting.

Example:

@GetMapping("/list")
public String listUsers(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "10") int size,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<User> userPage = userService.findAllUsers(pageable);
model.addAttribute("users", userPage.getContent());
model.addAttribute("totalPages", userPage.getTotalPages());
return "user-list";
}

Use Cache to Improve Performance

Caching is an effective technique to improve your application’s performance by reducing the number of calls made to the database or external services. Use Spring’s Cache abstraction to easily implement caching in your application.

Example:

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users");
}
}
@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Cacheable(value = "users", key = "#userId")
public User findUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}

@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
}

Use ModelMapper or MapStruct for DTO Mapping

When working with DTOs (Data Transfer Objects), use libraries like ModelMapper or MapStruct to simplify the conversion between your domain objects and DTOs.

Example with ModelMapper:

@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
return modelMapper;
}
@Autowired
private ModelMapper modelMapper;

public UserDto convertToDto(User user) {
return modelMapper.map(user, UserDto.class);
}

public User convertToEntity(UserDto userDto) {
return modelMapper.map(userDto, User.class);
}

Use Cross-Origin Resource Sharing (CORS) for Cross-Domain Requests

If your application serves resources to clients on different domains, configure CORS to control which domains are allowed to access your resources.

Example:

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}

Implement API Versioning

When building REST APIs, implement versioning to maintain backward compatibility and allow clients to transition smoothly between different API versions.

Example:

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {

// Controller methods for version 1
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {

// Controller methods for version 2
}

Use Profiles for Environment-Specific Configurations

Use Spring Profiles to maintain environment-specific configurations, such as development, staging, and production.

Example:

# application.yml
spring:
profiles:
active: dev

---

spring:
profiles: dev
datasource:
url: jdbc:h2:mem:devdb
username: sa
password:

---

spring:
profiles: prod
datasource:
url: jdbc:mysql://localhost:3306/proddb
username: user
password: pass

Implement Health Checks and Monitoring

Integrate health checks and monitoring solutions like Spring Boot Actuator to monitor your application’s health, performance, and other metrics.

Example:

<!-- Add Spring Boot Actuator to your pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always

In this blog post, we have discussed some best practices for working with Spring MVC. By following these guidelines and using the provided code examples, you can develop maintainable, scalable, and secure web applications with Spring MVC.

Thank You for your attention! Happy Learning!

--

--