Spring Boot is the most popular Java framework for building enterprise applications. Here are best practices for building production-ready Spring Boot applications.
1. Project Structure
Recommended Structure
src/
├── main/
│ ├── java/
│ │ └── com/example/
│ │ ├── Application.java
│ │ ├── config/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ ├── model/
│ │ └── dto/
│ └── resources/
│ ├── application.yml
│ └── application-prod.yml
└── test/
2. Configuration Management
Use YAML for Configuration
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
server:
port: 8080
error:
include-message: always
include-stacktrace: on_param
Profile-Based Configuration
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
jpa:
show-sql: true
# application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
jpa:
show-sql: false
3. Dependency Injection
Constructor Injection
// Good: Constructor injection
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
// Bad: Field injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
4. Exception Handling
Global Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
errors
);
return ResponseEntity.badRequest().body(error);
}
}
5. REST API Design
Controller Best Practices
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<UserDTO> users = userService.getAllUsers(page, size);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Valid @RequestBody CreateUserRequest request) {
UserDTO user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserDTO user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
6. Service Layer
Service Implementation
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException(
"User not found with id: " + id));
return userMapper.toDTO(user);
}
public UserDTO createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateResourceException(
"Email already exists");
}
User user = userMapper.toEntity(request);
user = userRepository.save(user);
return userMapper.toDTO(user);
}
}
7. Repository Layer
JPA Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findByStatus(@Param("status") UserStatus status);
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
}
8. Entity Design
JPA Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@Version
private Long version;
// Getters and setters
}
9. Validation
DTO Validation
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 120, message = "Age must be at most 120")
private Integer age;
// Getters and setters
}
10. Security
Spring Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
}
11. Testing
Unit Tests
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUser() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("Test User");
User savedUser = new User();
savedUser.setId(1L);
savedUser.setEmail(request.getEmail());
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
UserDTO result = userService.createUser(request);
// Then
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getEmail()).isEqualTo("[email protected]");
}
}
Integration Tests
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("Test User");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("[email protected]"));
}
}
12. Performance Optimization
Connection Pooling
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
Caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "posts");
}
}
// Usage
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public UserDTO getUserById(Long id) {
return userRepository.findById(id)
.map(userMapper::toDTO)
.orElseThrow();
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
13. Monitoring and Logging
Actuator Endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
Logging Configuration
logging:
level:
root: INFO
com.example: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
max-size: 10MB
max-history: 30
Best Practices Summary
- Use constructor injection for dependencies
- Implement global exception handling
- Validate all inputs with Bean Validation
- Use DTOs to separate API from domain models
- Implement proper logging and monitoring
- Use profiles for environment-specific config
- Write comprehensive tests
- Optimize database queries
- Implement caching where appropriate
- Follow RESTful conventions
Conclusion
Spring Boot best practices help you build:
- Maintainable applications
- Scalable systems
- Secure APIs
- Testable code
- Production-ready services
Follow these practices to build robust Spring Boot applications! ☕