Skip to content

Latest commit

 

History

History
632 lines (511 loc) · 17.7 KB

File metadata and controls

632 lines (511 loc) · 17.7 KB

Coding Standards - Java Spring Boot Boilerplate

AI Context: This is the single source of truth for Java/Spring Boot code conventions. For current status: see ../docs/status/progress.yaml For project overview: see ../docs/index.md

Code Style & Conventions

General Principles

  • Readability: Code should be self-documenting with clear intent
  • Consistency: Follow established Spring Boot patterns
  • Maintainability: Write code for future developers
  • Performance: Optimize for scalability and response time
  • Security: Implement secure coding practices (OWASP guidelines)
  • SOLID Principles: Follow SOLID principles in design
  • High Cohesion, Low Coupling: Maintain clear separation of concerns

Naming Conventions

Files & Directories

  • Controllers: PascalCase with Controller suffix (UserController.java)
  • Services: PascalCase with Service suffix (UserService.java)
  • Repositories: PascalCase with Repository suffix (UserRepository.java)
  • Models/Entities: PascalCase (User.java, Product.java)
  • DTOs: PascalCase with DTO suffix (UserDTO.java, CreateUserRequest.java)
  • Configurations: PascalCase with Config suffix (SecurityConfig.java)
  • Exceptions: PascalCase with Exception suffix (UserNotFoundException.java)
  • Utilities: PascalCase with Util or Helper suffix (DateUtil.java)

Package Structure

com.company.project
├── config/              # Configuration classes
├── controller/          # REST controllers
├── service/             # Business logic
│   └── impl/           # Service implementations
├── repository/          # Data access layer
├── model/              # Domain entities
│   ├── entity/         # JPA entities
│   └── dto/            # Data transfer objects
├── exception/          # Custom exceptions
├── security/           # Security related classes
├── util/               # Utility classes
└── validator/          # Custom validators

Variables & Methods

  • Variables: camelCase (userName, isActive)
  • Methods: camelCase (getUserById, createUser)
  • Boolean: Prefix with is, has, can (isActive, hasPermission)
  • Constants: UPPER_SNAKE_CASE (MAX_RETRY_ATTEMPTS, DEFAULT_PAGE_SIZE)
  • Classes: PascalCase (UserService, ProductRepository)
  • Interfaces: PascalCase without I prefix (UserService, not IUserService)

Java Standards

Java Version

  • Use Java 21 LTS features
  • Leverage modern Java features: records, sealed classes, pattern matching, text blocks

Code Style

// Class structure
package com.company.project.service;

import com.company.project.model.entity.User;
import com.company.project.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

/**
 * Service for managing user operations.
 * Handles business logic for user CRUD operations.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;

    /**
     * Find user by ID.
     *
     * @param id the user ID
     * @return Optional containing user if found
     */
    public Optional<User> findById(Long id) {
        log.debug("Finding user by id: {}", id);
        return userRepository.findById(id);
    }

    /**
     * Create new user.
     *
     * @param user the user to create
     * @return created user
     */
    @Transactional
    public User createUser(User user) {
        log.info("Creating new user: {}", user.getEmail());

        validateUser(user);
        User savedUser = userRepository.save(user);
        emailService.sendWelcomeEmail(savedUser);

        return savedUser;
    }

    private void validateUser(User user) {
        if (user == null || user.getEmail() == null) {
            throw new IllegalArgumentException("User and email must not be null");
        }
    }
}

Modern Java Features

// Records for DTOs
public record UserDTO(
    Long id,
    String name,
    String email,
    UserRole role
) {}

// Text blocks for queries
@Query("""
    SELECT u FROM User u
    WHERE u.active = true
    AND u.createdAt > :date
    ORDER BY u.name
    """)
List<User> findActiveUsersSince(@Param("date") LocalDateTime date);

// Pattern matching
if (user instanceof Admin admin && admin.hasPermission("DELETE")) {
    admin.deleteUser(userId);
}

// ##### OUT OF CONTEXT ###
// Sealed classes for type hierarchies
public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {}

Spring Boot Standards

Dependency Injection

// Prefer constructor injection
@Service
@RequiredArgsConstructor  // Lombok generates constructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
}

// Avoid field injection
@Service
public class UserService {
    @Autowired  // ❌ Avoid this
    private UserRepository userRepository;
}

Controller Layer

package com.company.project.controller;

import com.company.project.model.dto.UserDTO;
import com.company.project.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * REST controller for user management.
 */
@Tag(name = "Users", description = "User management endpoints")
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @Operation(summary = "Get user by ID")
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @Operation(summary = "Create new user")
    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDTO user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }

    @Operation(summary = "Update user")
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(
        @PathVariable Long id,
        @Valid @RequestBody UpdateUserRequest request
    ) {
        UserDTO user = userService.updateUser(id, request);
        return ResponseEntity.ok(user);
    }

    @Operation(summary = "Delete user")
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
    }
}

Service Layer

package com.company.project.service;

import com.company.project.exception.UserNotFoundException;
import com.company.project.model.dto.UserDTO;
import com.company.project.model.entity.User;
import com.company.project.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service for user business logic.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final UserMapper userMapper;

    @Cacheable(value = "users", key = "#id")
    @Transactional(readOnly = true)
    public UserDTO findById(Long id) {
        log.debug("Finding user by id: {}", id);
        return userRepository.findById(id)
            .map(userMapper::toDTO)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    @Transactional
    @CacheEvict(value = "users", allEntries = true)
    public UserDTO createUser(CreateUserRequest request) {
        log.info("Creating user with email: {}", request.email());

        User user = userMapper.toEntity(request);
        User savedUser = userRepository.save(user);

        return userMapper.toDTO(savedUser);
    }
}

Repository Layer

package com.company.project.repository;

import com.company.project.model.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

/**
 * Repository for User entity.
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);

    List<User> findByActiveTrue();

    @Query("SELECT u FROM User u WHERE u.createdAt > :date")
    List<User> findRecentUsers(@Param("date") LocalDateTime date);

    boolean existsByEmail(String email);
}

Entity Layer

package com.company.project.model.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

/**
 * User entity representing application users.
 */
@Entity
@Table(name = "users", indexes = {
    @Index(name = "idx_email", columnList = "email"),
    @Index(name = "idx_active", columnList = "active")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserRole role;

    @Column(nullable = false)
    @Builder.Default
    private Boolean active = true;

    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

Exception Handling

// Custom exception
package com.company.project.exception;

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(Long id) {
        super("User not found with id: " + id);
    }
}

// Global exception handler
package com.company.project.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;

/**
 * Global exception handler for REST controllers.
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
        log.error("User not found: {}", ex.getMessage());
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        log.error("Unexpected error: {}", ex.getMessage(), ex);
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "An unexpected error occurred",
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

record ErrorResponse(int status, String message, LocalDateTime timestamp) {}

Validation

// DTO with validation
package com.company.project.model.dto;

import jakarta.validation.constraints.*;

public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    String name,

    @NotBlank(message = "Email is required")
    @Email(message = "Email must be valid")
    String email,

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d@$!%*#?&]{8,}$",
             message = "Password must contain letters and numbers")
    String password
) {}

// Custom validator
package com.company.project.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
@Documented
public @interface UniqueEmail {
    String message() default "Email already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Logging Standards

// Use SLF4J with Lombok
@Slf4j
@Service
public class UserService {

    public UserDTO createUser(CreateUserRequest request) {
        log.info("Creating user with email: {}", request.email());

        try {
            // Business logic
            log.debug("User created successfully: {}", user.getId());
            return userMapper.toDTO(user);
        } catch (Exception e) {
            log.error("Error creating user: {}", request.email(), e);
            throw e;
        }
    }
}

// Log levels
// ERROR: Errors requiring immediate attention
// WARN: Potentially harmful situations
// INFO: Important business events
// DEBUG: Detailed information for debugging
// TRACE: Very detailed diagnostic information

Testing Standards

// Unit test with JUnit 5 and Mockito
package com.company.project.service;

import com.company.project.exception.UserNotFoundException;
import com.company.project.model.entity.User;
import com.company.project.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void findById_WhenUserExists_ReturnsUser() {
        // Given
        Long userId = 1L;
        User user = User.builder()
            .id(userId)
            .email("test@example.com")
            .name("Test User")
            .build();
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // When
        UserDTO result = userService.findById(userId);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.email()).isEqualTo("test@example.com");
        verify(userRepository).findById(userId);
    }

    @Test
    void findById_WhenUserNotExists_ThrowsException() {
        // Given
        Long userId = 1L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // When & Then
        assertThatThrownBy(() -> userService.findById(userId))
            .isInstanceOf(UserNotFoundException.class)
            .hasMessageContaining("User not found");
    }
}

// Integration test with Spring Boot Test
package com.company.project.controller;

import com.company.project.model.dto.CreateUserRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void createUser_WithValidData_ReturnsCreated() throws Exception {
        CreateUserRequest request = new CreateUserRequest(
            "Test User",
            "test@example.com",
            "password123"
        );

        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }
}

Code Quality

Lombok Usage

  • Use @RequiredArgsConstructor for dependency injection
  • Use @Slf4j for logging
  • Use @Data, @Getter, @Setter for entities/DTOs
  • Use @Builder for complex object creation
  • Avoid @AllArgsConstructor on entities (use @Builder instead)

Best Practices

  • Keep methods small and focused (< 20 lines ideally)
  • Use meaningful variable and method names
  • Write javadoc for public APIs
  • Validate input parameters
  • Handle exceptions appropriately
  • Use Optional instead of null returns
  • Prefer immutable objects where possible (records, final fields)
  • Use Spring's dependency injection
  • Follow RESTful conventions

Last Updated: 2025-10-08