Back to Blog
2025-01-018 min readBackend 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

  1. Follow Spring Boot conventions and best practices
  2. Use proper exception handling and validation
  3. Keep services focused and use domain events
  4. Write clear and meaningful tests
  5. 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.

Contact
Home