Spring MVC Best Practices with Code Snippets

Prashant BalePrashant Bale
6 min read

1. Use Constructor Injection

Prefer constructor injection over field injection for better testability and immutability.

Constructor injection ensures that the dependencies are provided when the object is created, making it easier to test and ensuring the object's state remains consistent.

Code Snippet:

@Controller
public class MyController {

    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    // Handler methods
}

2. Centralized Exception Handling

Use @ControllerAdvice to handle exceptions globally.

Centralizing exception handling keeps your controllers clean and ensures consistent error responses.

Code Snippet:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<String> handleResourceNotFoundException(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred");
    }
}

3. Validate User Input

Use @Valid and @Validated annotations to validate request objects.

Validating user input ensures that your application receives well-formed data, reducing the risk of errors and security issues.

Code Snippet:

public class User {

    @NotEmpty(message = "Name is required")
    private String name;

    @Email(message = "Email should be valid")
    private String email;

    // Getters and setters
}

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

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // Save user
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

4. Optimize Database Access

Use Spring Data JPA repositories and pagination to optimize database access.

Spring Data JPA simplifies data access and using pagination helps in handling large datasets efficiently.

Code Snippet:

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
}

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

    private final UserRepository userRepository;

    @Autowired
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public ResponseEntity<Page<User>> getAllUsers(@RequestParam int page, @RequestParam int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<User> users = userRepository.findAll(pageable);
        return ResponseEntity.ok(users);
    }
}

5. Implement Security

Use Spring Security to secure your web application.

Securing your application ensures that only authorized users can access certain endpoints, protecting sensitive data and operations.

Code Snippet:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password("{noop}password").roles("USER")
            .and()
            .withUser("admin").password("{noop}admin").roles("ADMIN");
    }
}

6. Use Thymeleaf for Templating

Use Thymeleaf for rendering views in Spring MVC applications.

Thymeleaf integrates well with Spring MVC and provides a natural templating approach, making it easier to work with HTML templates.

Code Snippet:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>My Spring MVC Application</title>
</head>
<body>
    <h1 th:text="'Welcome, ' + ${username} + '!'"></h1>
</body>
</html>
@Controller
public class HomeController {

    @GetMapping("/home")
    public String home(Model model) {
        model.addAttribute("username", "John Doe");
        return "home";
    }
}

7. Write Unit Tests

Write unit tests for your controllers, services, and repositories using JUnit and Mockito.

Unit testing ensures that each part of your application works correctly, reducing the risk of bugs and making refactoring safer.

Code Snippet:

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void testGetUserById() throws Exception {
        User user = new User(1L, "John Doe", "john@example.com");
        given(userService.findUserById(1L)).willReturn(user);

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John Doe"));
    }
}

8. Use ResponseEntity for Better Control Over HTTP Responses

Use ResponseEntity to provide better control over the HTTP responses, including status codes and headers.

ResponseEntity allows you to customize the HTTP response, making it easier to convey the correct status and any additional metadata.

Code Snippet:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        Product product = productService.findProductById(id);
        if (product != null) {
            return ResponseEntity.ok(product);
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product createdProduct = productService.saveProduct(product);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
    }
}

9. Implement DTOs (Data Transfer Objects)

Use DTOs to transfer data between the client and the server.

DTOs help in separating the internal data model from the external representation, providing a clear contract for data exchange.

Code Snippet:

public class UserDTO {
    private Long id;
    private String name;
    private String email;

    // Getters and setters
}

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

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        User user = userService.findUserById(id);
        if (user != null) {
            UserDTO userDTO = new UserDTO();
            BeanUtils.copyProperties(user, userDTO);
            return ResponseEntity.ok(userDTO);
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
    }

    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
        User user = new User();
        BeanUtils.copyProperties(userDTO, user);
        User createdUser = userService.saveUser(user);
        BeanUtils.copyProperties(createdUser, userDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(userDTO);
    }
}

10. Use Caching for Improved Performance

Use caching to improve the performance of your application by reducing the load on the database.

Caching frequently accessed data can significantly reduce the response times and load on your database.

Code Snippet:

@EnableCaching
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@Service
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable("products")
    public Product findProductById(Long id) {
        return productRepository.findById(id).orElse(null);
    }
}\

11. Use Asynchronous Processing

Use asynchronous processing for long-running tasks to improve the responsiveness of your application. Asynchronous processing allows you to handle long-running tasks in the background, freeing up the main thread to handle other requests.

Code Snippet:

@EnableAsync
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@Service
public class EmailService {

    @Async
    public void sendEmail(String to, String subject, String body) {
        // Simulate a long-running task
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // Send email logic
    }
}

@RestController
@RequestMapping("/api/notifications")
public class NotificationController {

    private final EmailService emailService;

    @Autowired
    public NotificationController(EmailService emailService) {
        this.emailService = emailService;
    }

    @PostMapping("/sendEmail")
    public ResponseEntity<String> sendEmail(@RequestParam String to, @RequestParam String subject, @RequestParam String body) {
        emailService.sendEmail(to, subject, body);
        return ResponseEntity.ok("Email is being sent");
    }
}

12. Use Profiles to Manage Environments

Use Spring Profiles to manage different environments (e.g., development, testing, production).

Profiles allow you to define different configurations for different environments, making it easier to manage environment-specific settings.

Code Snippet:

// application.yml
spring:
  profiles:
    active: dev

// application-dev.yml
datasource:
  url: jdbc:mysql://localhost:3306/devdb
  username: devuser
  password: devpass

// application-prod.yml
datasource:
  url: jdbc:mysql://prod-db:3306/proddb
  username: produser
  password: prodpass

@Configuration
@Profile("dev")
public class DevConfig {
    // Development-specific beans
}

@Configuration
@Profile("prod")
public class ProdConfig {
    // Production-specific beans
}

13. Use RESTful Conventions

Follow RESTful conventions for API design to make your APIs more intuitive and standard. Using standard RESTful conventions makes your APIs easier to understand and use for other developers.

Code Snippet:

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookService bookService;

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        List<Book> books = bookService.findAllBooks();
        return ResponseEntity.ok(books);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Book book = bookService.findBookById(id);
        if (book != null) {
            return ResponseEntity.ok(book);
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
    }

    @PostMapping
    public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
        Book createdBook = bookService.saveBook(book);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdBook);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @Valid @RequestBody Book book) {
        book.setId(id);
        Book updatedBook = bookService.saveBook(book);
        return ResponseEntity.ok(updatedBook);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }
}

Code

To provide you with a practical understanding, I've created a comprehensive GitHub repository that demonstrates these principles in action. The repository includes an application backend, showcasing key features like MVC Architecture, microservice architecture, JDBC for database interactions (Upgrade to JPA). Each part of the code is meticulously commented on to ensure clarity and ease of understanding. You can explore the repository here and follow along to enhance your Spring Boot MVC skills.

Conclusion

By following these best practices, you can build robust, maintainable, and secure Spring MVC applications. These practices ensure that your application is well-structured, efficient, and easy to manage.

Happy coding !!!

0
Subscribe to my newsletter

Read articles from Prashant Bale directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Prashant Bale
Prashant Bale

With 17+ years in software development and 14+ years specializing in Android app architecture and development, I am a seasoned Lead Android Developer. My comprehensive knowledge spans all phases of mobile application development, particularly within the banking domain. I excel at transforming business needs into secure, user-friendly solutions known for their scalability and durability. As a proven leader and Mobile Architect.