Java Expert
Virtual Threads (Project Loom)
-
Lightweight threads that dramatically improve scalability for I/O-bound applications
-
Use Executors.newVirtualThreadPerTaskExecutor() for thread pools
-
Perfect for web applications with many concurrent connections
-
Spring Boot 3.2+ supports virtual threads via configuration
// Enable virtual threads in Spring Boot 3.2+ // application.properties spring.threads.virtual.enabled=true
// Or programmatically @Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; }
// Using virtual threads directly try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { // I/O-bound task Thread.sleep(1000); return "result"; }); }
Pattern Matching
-
Pattern matching for switch (Java 21)
-
Record patterns
-
Destructuring with pattern matching
// Pattern matching for switch String result = switch (obj) { case String s -> "String: " + s; case Integer i -> "Integer: " + i; case Long l -> "Long: " + l; case null -> "null"; default -> "Unknown"; };
// Record patterns record Point(int x, int y) {}
if (obj instanceof Point(int x, int y)) { System.out.println("x: " + x + ", y: " + y); }
Records
-
Immutable data carriers
-
Automatically generates constructor, getters, equals(), hashCode(), toString()
public record UserDTO(String name, String email, LocalDate birthDate) { // Compact constructor for validation public UserDTO { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); } } }
Sealed Classes
-
Restrict which classes can extend/implement
-
Provides exhaustive pattern matching
public sealed interface Result<T> permits Success, Failure { record Success<T>(T value) implements Result<T> {} record Failure<T>(String error) implements Result<T> {} }
Spring Boot 3.x Best Practices (2026)
Framework Setup:
-
Java 21+ as baseline (virtual threads, pattern matching)
-
Spring Boot 3.2+ (latest stable)
-
Spring Framework 6.x
-
Jakarta EE (not javax.*) - namespace change
Project Structure (Layered Architecture):
src/main/java/com/example/app/ ├── controller/ # REST endpoints (RestController) ├── service/ # Business logic (Service) │ └── impl/ # Service implementations ├── repository/ # Data access (Repository) ├── model/ │ ├── entity/ # JPA entities │ └── dto/ # Data Transfer Objects ├── config/ # Configuration classes ├── exception/ # Custom exceptions and handlers └── util/ # Utility classes
Controller Layer (RestController):
-
Handle HTTP requests/responses only
-
Delegate business logic to services
-
Use DTOs for request/response bodies
-
Never directly inject repositories
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserDTO dto) {
UserDTO created = userService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
Service Layer:
-
Contains business logic
-
Uses repositories for data access
-
Converts between entities and DTOs
-
Annotated with @Service
@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final ModelMapper modelMapper;
@Override
public UserDTO findById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
return modelMapper.map(user, UserDTO.class);
}
@Override
@Transactional
public UserDTO create(CreateUserDTO dto) {
User user = modelMapper.map(dto, User.class);
User saved = userRepository.save(user);
return modelMapper.map(saved, UserDTO.class);
}
}
Repository Layer (Spring Data JPA):
-
Extends JpaRepository<Entity, ID>
-
Define custom query methods
-
Use @Query for complex queries
@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
// Projection for performance
@Query("SELECT new com.example.dto.UserSummaryDTO(u.id, u.name, u.email) FROM User u")
List<UserSummaryDTO> findAllSummaries();
}
JPA/Hibernate Best Practices
Entity Design:
-
Use @Entity and @Table annotations
-
Always define @Id with generation strategy
-
Use @Column for constraints and mappings
-
Implement equals() and hashCode() based on business key
@Entity @Table(name = "users") @Getter @Setter @NoArgsConstructor 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;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
Performance Optimization:
-
Use @EntityGraph or JOIN FETCH to prevent N+1 queries
-
Lazy load associations by default
-
Use pagination for large result sets
-
Define proper indexes in database
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id") Optional<User> findByIdWithOrders(@Param("id") Long id);
// Pagination Page<User> findAll(Pageable pageable);
Testing (JUnit 5 + Mockito)
Unit Testing Services:
@ExtendWith(MockitoExtension.class) class UserServiceImplTest { @Mock private UserRepository userRepository;
@Mock
private ModelMapper modelMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
void findById_WhenUserExists_ReturnsUserDTO() {
// Given
Long userId = 1L;
User user = new User();
user.setId(userId);
UserDTO expectedDTO = new UserDTO();
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
when(modelMapper.map(user, UserDTO.class)).thenReturn(expectedDTO);
// When
UserDTO result = userService.findById(userId);
// Then
assertNotNull(result);
verify(userRepository).findById(userId);
verify(modelMapper).map(user, UserDTO.class);
}
}
Integration Testing (Spring Boot Test):
@SpringBootTest @AutoConfigureMockMvc @Transactional class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void createUser_WithValidData_ReturnsCreated() throws Exception {
CreateUserDTO dto = new CreateUserDTO("John Doe", "john@example.com");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("John Doe"));
}
}
Build Tools (Maven & Gradle)
Maven (pom.xml):
<properties> <java.version>21</java.version> <spring-boot.version>3.2.0</spring-boot.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
Gradle (build.gradle):
plugins { id 'java' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' }
java { sourceCompatibility = JavaVersion.VERSION_21 }
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
Exception Handling
Global Exception Handler:
@RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage
));
ErrorResponse error = new ErrorResponse(
"VALIDATION_ERROR",
"Invalid input",
errors,
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
Logging and Monitoring
Logging (SLF4J + Logback):
@Slf4j @Service public class UserServiceImpl implements UserService {
public UserDTO findById(Long id) {
log.debug("Finding user with id: {}", id);
try {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
log.info("User found: {}", user.getEmail());
return modelMapper.map(user, UserDTO.class);
} catch (UserNotFoundException ex) {
log.error("User not found with id: {}", id, ex);
throw ex;
}
}
}
Actuator for Monitoring:
management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always
Iron Laws
-
ALWAYS use constructor injection over field injection with @Autowired — field injection hides dependencies, makes testing harder, and creates partially-initialized objects that crash at runtime if the context isn't fully loaded.
-
NEVER use Optional.get() without a preceding isPresent() check or orElse() /orElseThrow() — unconditional get() throws NoSuchElementException on empty optionals, silently defeating Optional's entire purpose.
-
ALWAYS handle @Transactional boundaries explicitly — calling a transactional method from within the same class bypasses the proxy and runs without a transaction, causing silent data inconsistency.
-
NEVER use @Async without a configured TaskExecutor — Spring's default @Async executor uses a single-thread pool; concurrent async calls queue up and defeat parallelism.
-
ALWAYS use @ControllerAdvice with specific exception types for error handling — catching Exception globally hides root causes; specific exception handlers produce correct HTTP status codes and meaningful error responses.
Anti-Patterns
Anti-Pattern Why It Fails Correct Approach
Field injection with @Autowired
Hidden dependencies; untestable without Spring context; null in unit tests Constructor injection; all required dependencies declared as final fields
Optional.get() without check NoSuchElementException at runtime; defeats Optional's null-safety contract Use orElseThrow() , orElse() , or map() /flatMap() chains
@Transactional on same-class method calls Spring proxy bypassed; method runs outside transaction; data integrity lost Move transactional methods to a separate service bean; inject and call from outside
Default @Async thread pool Single-thread pool queues all tasks; async calls run sequentially Configure ThreadPoolTaskExecutor with pool size, queue, and rejection policy
Global @ExceptionHandler(Exception.class)
Swallows specific exceptions; all errors return same generic 500 response Map specific exception types to HTTP status codes; use @ResponseStatus annotations
Consolidated Skills
This expert skill consolidates 1 individual skills:
- java-expert
Memory Protocol (MANDATORY)
Before starting:
cat .claude/context/memory/learnings.md
After completing: Record any new patterns or exceptions discovered.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.