Skip to main content

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
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;

@Autowired
private EmailService emailService;

// Service methods
}
java
@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)

java
@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)

java
@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
}
}
java
@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

java
@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

java
@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

java
// 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.

java
@Configuration
public class AppConfig {
@Bean
@Lazy
public ExpensiveService expensiveService() {
return new ExpensiveService();
}
}

Or at the component level:

java
@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

java
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:

java
@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:

java
@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;
}
}
java
@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)

java
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)

java
@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:

java
@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:

java
@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:

yaml
# 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

java
@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...
}
java
@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:

  1. Use constructor injection for cleaner dependency management
  2. Leverage @ConfigurationProperties for structured configuration
  3. Choose appropriate bean scopes for different use cases
  4. Follow a clear package structure for better organization
  5. Keep controllers thin by delegating to services
  6. Use DTOs to clearly separate API contracts from internal models
  7. Apply lazy initialization for performance optimization
  8. Optimize database access with Spring Data features
  9. Implement caching for expensive operations
  10. Write comprehensive tests at multiple levels
  11. Use profiles for environment-specific configuration
  12. Apply proper security measures using Spring Security
  13. 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

Exercises

  1. Refactor Practice: Take an existing Spring application and refactor it to use constructor injection instead of field injection.
  2. Configuration Migration: Convert a configuration using multiple @Value annotations to use @ConfigurationProperties.
  3. API Design: Create a REST API with proper separation between entities and DTOs, including mapping logic.
  4. Caching Implementation: Add caching to a service method that performs an expensive operation or database query.
  5. 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! :)