Spring Best Practices
Introduction
The Spring Framework has become one of the most popular Java frameworks for enterprise application development. While Spring makes it easier to build robust applications, following best practices ensures that your code remains clean, maintainable, and performant. This guide explores essential Spring best practices that will help you avoid common pitfalls and develop high-quality applications.
Whether you're just starting with Spring or looking to improve your existing Spring applications, these guidelines will help you make the most of what the Spring ecosystem offers.
Core Best Practices
1. Favor Constructor Injection Over Field Injection
Why It Matters
Dependency Injection (DI) is a fundamental concept in Spring. There are three main types of dependency injection:
- Constructor Injection
- Setter Injection
- Field Injection (using
@Autowired
directly on fields)
Constructor injection is generally preferred because it:
- Makes dependencies explicit
- Ensures required dependencies are not null
- Makes your components immutable
- Simplifies unit testing
Example: Field Injection (Not Recommended)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
// Service methods
}
Example: Constructor Injection (Recommended)
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
// Service methods
}
Note that with Spring 4.3+, you don't even need to add @Autowired
to the constructor if it's the only constructor in your class.
2. Use Configuration Properties for External Configuration
Instead of using @Value
annotations scattered throughout your code, use structured configuration with @ConfigurationProperties
.
Example: Using @Value (Not Ideal)
@Component
public class EmailService {
@Value("${email.server.host}")
private String host;
@Value("${email.server.port}")
private int port;
@Value("${email.from}")
private String fromAddress;
// Service code
}
Example: Using @ConfigurationProperties (Better Approach)
@Configuration
@ConfigurationProperties(prefix = "email")
public class EmailProperties {
private String from;
private Server server = new Server();
// Getters and setters
public static class Server {
private String host;
private int port;
// Getters and setters
}
}
@Service
public class EmailService {
private final EmailProperties emailProperties;
public EmailService(EmailProperties emailProperties) {
this.emailProperties = emailProperties;
}
// Service code using emailProperties.getFrom(),
// emailProperties.getServer().getHost(), etc.
}
3. Use Appropriate Bean Scopes
Spring beans are singleton by default, but sometimes other scopes are more appropriate:
- Singleton (default): One instance per Spring container
- Prototype: New instance each time requested
- Request: One instance per HTTP request
- Session: One instance per HTTP session
- Application: One instance per ServletContext
- WebSocket: One instance per WebSocket
Example: Using Different Bean Scopes
@Component
@Scope("singleton")
public class UserPreferences {
// This is actually redundant as singleton is the default
}
@Component
@Scope("prototype")
public class ShoppingCart {
// New instance for each user
}
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSessionInfo {
// One instance per HTTP request
}
Application Structure Best Practices
4. Follow a Clear Package Structure
Organize your code in a logical, consistent way. Here's a common approach:
com.example.myapp/
├── config/ // Configuration classes
├── controller/ // REST/Web controllers
├── service/ // Business logic
├── repository/ // Data access
├── model/ // Domain objects/entities
├── dto/ // Data Transfer Objects
├── exception/ // Custom exceptions
└── util/ // Utility classes
5. Keep Controllers Thin
Controllers should handle HTTP requests and delegate business logic to service classes.
Example: Thin Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserDto userDto) {
return new ResponseEntity<>(userService.createUser(userDto), HttpStatus.CREATED);
}
// Other endpoints...
}
6. Use DTOs for API Requests and Responses
Don't expose your internal domain entities directly to clients. Use Data Transfer Objects (DTOs) instead.
Example: Using DTOs
// Entity (internal model)
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
private String email;
private String passwordHash; // We don't want to expose this!
private LocalDateTime createdAt;
// Getters and setters
}
// DTO (public API model)
public class UserDto {
private Long id;
private String username;
private String email;
// No sensitive fields
// Getters and setters
}
Performance Best Practices
7. Use Lazy Initialization When Appropriate
By default, Spring initializes all singleton beans at startup. For beans that are rarely used or expensive to create, consider lazy initialization.
@Configuration
public class AppConfig {
@Bean
@Lazy
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
}
Or at the component level:
@Component
@Lazy
public class ExpensiveComponent {
// This bean will only be initialized when it's first requested
}
8. Optimize Database Access
Use Spring Data JPA features wisely:
Example: Use Projections for Selecting Specific Fields
public interface UserRepository extends JpaRepository<User, Long> {
interface UserSummary {
Long getId();
String getUsername();
String getEmail();
}
List<UserSummary> findAllProjectedBy();
@Query("SELECT u FROM User u WHERE u.active = true")
Page<User> findActiveUsers(Pageable pageable);
}
Using the repository:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<UserRepository.UserSummary> getAllUserSummaries() {
return userRepository.findAllProjectedBy();
}
public Page<User> getActiveUsersPaged(int page, int size) {
return userRepository.findActiveUsers(PageRequest.of(page, size));
}
}
9. Use Caching for Expensive Operations
Spring provides a powerful caching abstraction:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("users"),
new ConcurrentMapCache("products")
));
return cacheManager;
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable("products")
public Product getProductById(Long id) {
// This will only be executed if the product is not in the cache
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepository.save(product);
}
}
Testing Best Practices
10. Write Tests at Multiple Levels
Spring supports various testing strategies:
Unit Tests (Testing Components in Isolation)
public class UserServiceTest {
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testGetUserById() {
// Arrange
User mockUser = new User();
mockUser.setId(1L);
mockUser.setUsername("testuser");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// Act
User result = userService.getUserById(1L);
// Assert
assertNotNull(result);
assertEquals("testuser", result.getUsername());
verify(userRepository).findById(1L);
}
}
Integration Tests (Testing Multiple Components Together)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
public void testGetUser() throws Exception {
UserDto mockUserDto = new UserDto();
mockUserDto.setId(1L);
mockUserDto.setUsername("testuser");
when(userService.getUserById(1L)).thenReturn(mockUserDto);
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("testuser"));
}
}
11. Use @Profile for Different Environments
Configure different beans for different environments:
@Configuration
@Profile("development")
public class DevDataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
@Configuration
@Profile("production")
public class ProdDataSourceConfig {
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}
}
Security Best Practices
12. Apply Proper Security Measures
Always use Spring Security to protect your application:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable() // In a real application, carefully consider before disabling CSRF
.httpBasic();
}
}
13. Don't Store Sensitive Data in Properties Files
For sensitive information like API keys or database passwords, use environment variables or a proper secrets management solution:
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME} # From environment variable
password: ${DB_PASSWORD} # From environment variable
Real-World Application Example
Let's see how these best practices come together in a simplified e-commerce application:
Project Structure
com.example.shop/
├── config/
│ ├── AppConfig.java
│ ├── SecurityConfig.java
│ └── WebConfig.java
├── controller/
│ ├── ProductController.java
│ └── OrderController.java
├── service/
│ ├── ProductService.java
│ └── OrderService.java
├── repository/
│ ├── ProductRepository.java
│ └── OrderRepository.java
├── model/
│ ├── Product.java
│ └── Order.java
├── dto/
│ ├── ProductDto.java
│ └── OrderDto.java
└── exception/
└── ResourceNotFoundException.java
Sample Code: Product Controller and Service
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<ProductDto>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
@GetMapping("/{id}")
public ResponseEntity<ProductDto> getProductById(@PathVariable Long id) {
return ResponseEntity.ok(productService.getProductById(id));
}
@PostMapping
public ResponseEntity<ProductDto> createProduct(@Valid @RequestBody ProductDto productDto) {
return new ResponseEntity<>(productService.createProduct(productDto), HttpStatus.CREATED);
}
// Other endpoints...
}
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ModelMapper modelMapper; // For mapping entities to DTOs
public ProductService(ProductRepository productRepository, ModelMapper modelMapper) {
this.productRepository = productRepository;
this.modelMapper = modelMapper;
}
@Cacheable("products")
public List<ProductDto> getAllProducts() {
return productRepository.findAll().stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Cacheable(value = "product", key = "#id")
public ProductDto getProductById(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id " + id));
return convertToDto(product);
}
@CacheEvict(value = {"products", "product"}, allEntries = true)
public ProductDto createProduct(ProductDto productDto) {
Product product = convertToEntity(productDto);
Product savedProduct = productRepository.save(product);
return convertToDto(savedProduct);
}
private ProductDto convertToDto(Product product) {
return modelMapper.map(product, ProductDto.class);
}
private Product convertToEntity(ProductDto productDto) {
return modelMapper.map(productDto, Product.class);
}
// Other service methods...
}
Summary
In this guide, we've covered key Spring best practices that will help you create more maintainable, efficient, and secure applications:
- Use constructor injection for cleaner dependency management
- Leverage @ConfigurationProperties for structured configuration
- Choose appropriate bean scopes for different use cases
- Follow a clear package structure for better organization
- Keep controllers thin by delegating to services
- Use DTOs to clearly separate API contracts from internal models
- Apply lazy initialization for performance optimization
- Optimize database access with Spring Data features
- Implement caching for expensive operations
- Write comprehensive tests at multiple levels
- Use profiles for environment-specific configuration
- Apply proper security measures using Spring Security
- Protect sensitive data with proper secrets management
Following these best practices will not only make your Spring applications more robust but also more maintainable in the long run. As you grow more comfortable with Spring, you'll develop your own patterns and practices that work for your specific use cases.
Additional Resources
- Spring Framework Documentation
- Spring Boot Reference Documentation
- Spring Security Reference
- Spring Data JPA Documentation
- Testing Spring Boot Applications
Exercises
- Refactor Practice: Take an existing Spring application and refactor it to use constructor injection instead of field injection.
- Configuration Migration: Convert a configuration using multiple @Value annotations to use @ConfigurationProperties.
- API Design: Create a REST API with proper separation between entities and DTOs, including mapping logic.
- Caching Implementation: Add caching to a service method that performs an expensive operation or database query.
- Test Coverage: Write unit tests and integration tests for a Spring service and its corresponding controller.
By implementing these exercises, you'll gain practical experience applying Spring best practices in real-world scenarios.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)