AI Context: This is the single source of truth for Java/Spring Boot code conventions. For current status: see
../docs/status/progress.yamlFor project overview: see../docs/index.md
- 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
- Controllers: PascalCase with
Controllersuffix (UserController.java) - Services: PascalCase with
Servicesuffix (UserService.java) - Repositories: PascalCase with
Repositorysuffix (UserRepository.java) - Models/Entities: PascalCase (
User.java,Product.java) - DTOs: PascalCase with
DTOsuffix (UserDTO.java,CreateUserRequest.java) - Configurations: PascalCase with
Configsuffix (SecurityConfig.java) - Exceptions: PascalCase with
Exceptionsuffix (UserNotFoundException.java) - Utilities: PascalCase with
UtilorHelpersuffix (DateUtil.java)
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: 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
Iprefix (UserService, notIUserService)
- Use Java 21 LTS features
- Leverage modern Java features: records, sealed classes, pattern matching, text blocks
// 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");
}
}
}// 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 {}// 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;
}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);
}
}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);
}
}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);
}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;
}// 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) {}// 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 {};
}// 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// 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"));
}
}- Use
@RequiredArgsConstructorfor dependency injection - Use
@Slf4jfor logging - Use
@Data,@Getter,@Setterfor entities/DTOs - Use
@Builderfor complex object creation - Avoid
@AllArgsConstructoron entities (use@Builderinstead)
- 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