2025-01-01•8 min read•Backend Development
Clean Code Practices in Java: A Spring Boot Developer's Guide
javaspring-bootclean-codebest-practicesbackend
Clean Code Practices in Java: A Spring Boot Developer's Guide
As Spring Boot developers, we're not just building applications; we're creating codebases that our fellow developers will work with for years to come. Let's explore how to write clean, maintainable Java code that makes life easier for everyone on the team.
Core Principles of Clean Java Code
1. Clear Class Structure
// Bad: Mixing concerns and unclear responsibilities
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private PaymentService paymentService;
public void registerUser(UserDTO dto) {
User user = new User(dto);
userRepository.save(user);
emailService.sendWelcomeEmail(user);
paymentService.setupBillingAccount(user);
}
}
// Good: Single Responsibility and Clear Structure
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
private final UserRepository userRepository;
private final UserRegistrationEventPublisher eventPublisher;
@Transactional
public User registerUser(UserRegistrationRequest request) {
User user = UserMapper.toEntity(request);
validateUserData(user);
User savedUser = userRepository.save(user);
eventPublisher.publishUserRegistered(savedUser);
return savedUser;
}
}
2. Effective Exception Handling
// Bad: Generic exception handling
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
try {
return ResponseEntity.ok(userService.getUser(id));
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
// Good: Specific exception handling with custom exceptions
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
try {
UserResponse user = userService.getUser(id);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
} catch (UserAccessDeniedException e) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
}
}
3. Clean Repository Pattern
// Bad: Complex queries in repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.email = ?1 AND u.status = 'ACTIVE' " +
"AND u.lastLoginDate > ?2 AND u.subscriptionType = ?3")
Optional<User> findActiveUserByEmailAndSubscription(String email,
LocalDateTime date, String subscriptionType);
}
// Good: Using specification pattern for complex queries
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
Optional<User> findByEmail(String email);
}
// Separate specification class
public class UserSpecifications {
public static Specification<User> isActiveUser() {
return (root, query, cb) -> cb.equal(root.get("status"), "ACTIVE");
}
public static Specification<User> hasRecentLogin(LocalDateTime date) {
return (root, query, cb) -> cb.greaterThan(root.get("lastLoginDate"), date);
}
}
Spring Boot Best Practices
1. Configuration Management
// Bad: Hardcoded configuration
@Service
public class EmailService {
private final String smtpServer = "smtp.company.com";
private final int port = 587;
public void sendEmail() {
// Using hardcoded values
}
}
// Good: Using configuration properties
@ConfigurationProperties(prefix = "mail")
@Validated
public record MailProperties(
@NotBlank String smtpServer,
@Min(1) @Max(65535) int port,
@NotBlank String username,
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$")
String fromAddress
) {}
@Service
@RequiredArgsConstructor
public class EmailService {
private final MailProperties mailProperties;
}
2. REST API Design
// Bad: Inconsistent API responses
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public User createUser(@RequestBody UserDTO dto) {
return userService.createUser(dto);
}
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUser(id));
}
}
// Good: Consistent API responses with proper DTOs
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<UserResponse> createUser(
@Valid @RequestBody UserCreateRequest request) {
UserResponse user = userService.createUser(request);
return ApiResponse.success(user);
}
@GetMapping("/{id}")
public ApiResponse<UserResponse> getUser(@PathVariable Long id) {
UserResponse user = userService.getUser(id);
return ApiResponse.success(user);
}
}
3. Service Layer Organization
// Bad: Mixed business logic
@Service
public class OrderService {
public Order createOrder(OrderDTO dto) {
validateStock(dto);
calculatePrice(dto);
updateInventory(dto);
notifyUser(dto);
return saveOrder(dto);
}
}
// Good: Using domain events and separate services
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final OrderDomainService domainService;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public OrderResponse createOrder(OrderCreateRequest request) {
Order order = domainService.validateAndCreateOrder(request);
Order savedOrder = orderRepository.save(order);
eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder));
return OrderMapper.toResponse(savedOrder);
}
}
Testing Best Practices
1. Unit Testing
// Bad: Unclear test structure
@Test
void testUserCreation() {
User user = new User();
user.setName("John");
userService.save(user);
assertTrue(userService.findById(user.getId()).isPresent());
}
// Good: Clear test structure with BDD style
@Test
void givenValidUserData_whenCreatingUser_thenUserIsSavedSuccessfully() {
// Given
UserCreateRequest request = UserCreateRequest.builder()
.name("John Doe")
.email("john@example.com")
.build();
// When
UserResponse response = userService.createUser(request);
// Then
assertThat(response)
.isNotNull()
.satisfies(user -> {
assertThat(user.getName()).isEqualTo("John Doe");
assertThat(user.getEmail()).isEqualTo("john@example.com");
});
}
Conclusion
Writing clean Java code in Spring Boot applications is about creating maintainable, readable, and robust systems. Remember:
"The only valid measurement of code quality is WTFs per minute" - Robert Martin
Key Takeaways
- Follow Spring Boot conventions and best practices
- Use proper exception handling and validation
- Keep services focused and use domain events
- Write clear and meaningful tests
- Use configuration properties instead of hardcoding values
By following these practices, we create Spring Boot applications that are not only functional but also a joy to work with for the entire development team.