--- name: java-patterns description: "Java: Spring Boot, CompletableFuture, records, sealed types, JPA/Hibernate, virtual threads. Triggers: Java, Spring, JPA, Hibernate, Maven, Gradle, virtual thread, sealed class." effort: medium user-invocable: false allowed-tools: Read --- # Java Patterns Skill ## Project Structure ### Maven / Gradle Standard Layout ``` my-app/ ├── pom.xml (or build.gradle.kts + settings.gradle.kts) ├── src/ │ ├── main/ │ │ ├── java/com/example/myapp/ │ │ │ ├── MyApplication.java │ │ │ ├── config/ │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── repository/ │ │ │ ├── model/ │ │ │ │ ├── entity/ │ │ │ │ └── dto/ │ │ │ └── exception/ │ │ └── resources/ │ │ ├── application.yml │ │ └── db/migration/ │ └── test/ │ ├── java/com/example/myapp/ │ └── resources/application-test.yml └── target/ (or build/) ``` ### Multi-Module ``` parent/ ├── pom.xml (packaging=pom) ├── common/ (shared utilities) ├── domain/ (entities, business rules) ├── api/ (REST controllers, DTOs) └── app/ (Spring Boot main, wiring) ``` --- ## Idioms / Code Style ### Records (Java 16+) ```java public record UserDto(Long id, String name, String email) { public UserDto { // compact constructor for validation Objects.requireNonNull(name, "name must not be null"); Objects.requireNonNull(email, "email must not be null"); } } ``` ### Sealed Classes (Java 17+) ```java public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); } public record Circle(double radius) implements Shape { public double area() { return Math.PI * radius * radius; } } public record Rectangle(double w, double h) implements Shape { public double area() { return w * h; } } public record Triangle(double base, double height) implements Shape { public double area() { return 0.5 * base * height; } } ``` ### Switch Expressions (Java 14+) ```java String describe(Shape shape) { return switch (shape) { case Circle c -> "Circle r=" + c.radius(); case Rectangle r -> "Rect %sx%s".formatted(r.w(), r.h()); case Triangle t -> "Triangle base=" + t.base(); }; } // Guard patterns (Java 21+) String classify(Shape shape) { return switch (shape) { case Circle c when c.radius() > 100 -> "large circle"; case Circle c -> "small circle"; case Rectangle r -> "rectangle"; case Triangle t -> "triangle"; }; } ``` ### var, Streams, Optional ```java // var -- use when RHS makes type obvious var users = new ArrayList(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Avoid: var result = service.process(data); -- type unclear // Streams List names = users.stream() .filter(User::isActive) .map(User::name) .sorted() .toList(); // Java 16+, unmodifiable Map> byDept = users.stream() .collect(Collectors.groupingBy(User::department)); // Optional -- return type only, never as field or parameter String city = findByEmail(email) .map(User::address) .map(Address::city) .orElse("Unknown"); // Never call .get() without guard -- use orElse/orElseThrow // Text blocks (Java 15+) String json = """ {"name": "%s", "email": "%s"} """.formatted(name, email); ``` --- ## Error Handling | Type | When | Examples | |------|------|---------| | Checked | Recoverable I/O the caller must handle | IOException, SQLException | | Unchecked | Programming errors, business rule violations | IllegalArgumentException, custom domain exceptions | ### Custom Exception Hierarchy ```java public abstract class DomainException extends RuntimeException { private final String errorCode; protected DomainException(String errorCode, String message) { super(message); this.errorCode = errorCode; } public String errorCode() { return errorCode; } } public class EntityNotFoundException extends DomainException { public EntityNotFoundException(String entity, Object id) { super("NOT_FOUND", "%s with id %s not found".formatted(entity, id)); } } ``` ### Try-With-Resources ```java try (var conn = dataSource.getConnection(); var stmt = conn.prepareStatement(sql); var rs = stmt.executeQuery()) { while (rs.next()) { results.add(mapRow(rs)); } } ``` ### Global Handler (Spring) ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleNotFound(EntityNotFoundException ex) { return ResponseEntity.status(404).body(new ErrorResponse(ex.errorCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException ex) { var errors = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()).toList(); return ResponseEntity.badRequest().body(new ErrorResponse("VALIDATION_ERROR", errors.toString())); } @ExceptionHandler(Exception.class) public ResponseEntity handleGeneric(Exception ex) { log.error("Unhandled exception", ex); return ResponseEntity.internalServerError() .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred")); } } public record ErrorResponse(String code, String message) {} ``` --- ## Testing Patterns ### JUnit 5 + Mockito + AssertJ ```java @DisplayName("UserService") class UserServiceTest { private UserRepository repository; private UserService service; @BeforeEach void setUp() { repository = mock(UserRepository.class); service = new UserService(repository); } @Test void createsUserWithValidData() { var request = new CreateUserRequest("Alice", "alice@example.com"); when(repository.save(any())).thenAnswer(inv -> inv.getArgument(0)); var user = service.createUser(request); assertThat(user.name()).isEqualTo("Alice"); verify(repository).save(any(User.class)); } @Test void throwsWhenEmailExists() { when(repository.existsByEmail("taken@test.com")).thenReturn(true); assertThatThrownBy(() -> service.createUser(new CreateUserRequest("Bob", "taken@test.com"))) .isInstanceOf(BusinessRuleViolationException.class) .hasMessageContaining("email already exists"); } } ``` ### Parameterized Tests ```java @ParameterizedTest @CsvSource({"1,1,2", "0,0,0", "-1,1,0", "100,200,300"}) void addReturnsSumOfArguments(int a, int b, int expected) { assertThat(calculator.add(a, b)).isEqualTo(expected); } @ParameterizedTest @MethodSource("invalidEmails") void rejectsInvalidEmail(String email) { assertThatThrownBy(() -> new Email(email)).isInstanceOf(IllegalArgumentException.class); } static Stream invalidEmails() { return Stream.of("", "no-at-sign", "@no-local", "spaces in@email.com"); } ``` ### Mockito Extras ```java // Argument captor var captor = ArgumentCaptor.forClass(User.class); verify(repository).save(captor.capture()); assertThat(captor.getValue().name()).isEqualTo("Alice"); // BDD style given(repository.findById(1L)).willReturn(Optional.of(user)); then(repository).should().findById(1L); ``` ### Testcontainers (Integration) ```java @Testcontainers @SpringBootTest class UserRepositoryIT { @Container static PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:16-alpine"); @DynamicPropertySource static void props(DynamicPropertyRegistry r) { r.add("spring.datasource.url", pg::getJdbcUrl); r.add("spring.datasource.username", pg::getUsername); r.add("spring.datasource.password", pg::getPassword); } @Autowired UserRepository repository; @Test void savesAndRetrievesUser() { var saved = repository.save(new User("Alice", "alice@test.com")); assertThat(repository.findById(saved.getId())).isPresent() .get().extracting("name").isEqualTo("Alice"); } } ``` --- ## Common Frameworks ### Spring Boot (Controller / Service / Repository) ```java @RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping public List list(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { return userService.list(PageRequest.of(page, size)); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserDto create(@Valid @RequestBody CreateUserRequest request) { return userService.create(request); } } @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class UserService { private final UserRepository repository; private final UserMapper mapper; @Transactional public UserDto create(CreateUserRequest request) { if (repository.existsByEmail(request.email())) throw new BusinessRuleViolationException("email already exists"); return mapper.toDto(repository.save(mapper.toEntity(request))); } } public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.active = true AND u.role = :role") List findActiveByRole(@Param("role") Role role); } ``` ### Quarkus Uses Jakarta REST (`@Path`, `@GET`, `@POST`) + CDI (`@Inject`, `@ApplicationScoped`). Panache simplifies JPA: `implements PanacheRepository` gives `find()`, `persist()`, `list()` out of the box. Use `@Transactional` on mutating endpoints. ### Jackson, Lombok, MapStruct ```java // Jackson -- snake_case response @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record ApiResponse(T data, @JsonInclude(Include.NON_NULL) String error) {} // Lombok -- JPA entities only (use records for DTOs) @Entity @Getter @Setter @NoArgsConstructor(access = PROTECTED) @Builder @ToString(exclude = "password") @EqualsAndHashCode(of = "id") public class User { @Id @GeneratedValue(strategy = IDENTITY) private Long id; private String name; private String email; private String password; } // MapStruct @Mapper(componentModel = "spring") public interface UserMapper { UserDto toDto(User entity); User toEntity(CreateUserRequest request); } ``` --- ## Performance Tips ### JVM Tuning ```bash # Container-friendly (Java 17+) java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar app.jar # GC: -XX:+UseZGC (low latency) | -XX:+UseG1GC (balanced, default) | -XX:+UseParallelGC (throughput) ``` ### Virtual Threads (Java 21+) ```java try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var futures = urls.stream().map(url -> executor.submit(() -> fetch(url))).toList(); var results = futures.stream().map(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } }).toList(); } // Spring Boot 3.2+: spring.threads.virtual.enabled=true ``` ### Profiling with JFR ```bash java -XX:StartFlightRecording=duration=60s,filename=profile.jfr -jar app.jar jfr print --events jdk.CPULoad,jdk.GCHeapSummary profile.jfr ``` ### Common Pitfalls | Pitfall | Fix | |---------|-----| | String concat in loop | `StringBuilder` or `String.join()` | | Unbounded caches | `Caffeine` with `maximumSize` + `expireAfterWrite` | | Autoboxing in hot path | Primitive streams (`mapToInt`) or primitive collections | | Synchronized everything | `ConcurrentHashMap`, `StampedLock`, virtual threads | | Reflection in tight loop | Cache `MethodHandle` or use code generation | --- ## Build / Package Management ### Maven (key elements) ```xml 21 org.springframework.boot spring-boot-starter-parent 3.3.0 ``` ### Gradle Kotlin DSL ```kotlin plugins { java; id("org.springframework.boot") version "3.3.0" } java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.test { useJUnitPlatform() } ``` ### Dependency Management | Concern | Approach | |---------|----------| | Version alignment | BOM import via `dependencyManagement` / `platform()` | | Vulnerability scan | OWASP plugin (`dependencyCheckAnalyze`) | | Unused deps | `mvn dependency:analyze` or Gradle `dependency-analysis` plugin | | Reproducible builds | Pin plugin versions, `maven-enforcer-plugin` | --- ## Anti-Patterns | Anti-Pattern | Problem | Solution | |--------------|---------|----------| | God service class | 1000+ lines | Split by domain concern | | Anemic domain model | Logic only in services | Put behavior on domain objects | | Catching `Exception` | Hides bugs | Catch specific types | | `@Autowired` on fields | Hidden deps, untestable | Constructor injection | | Mutable DTOs | Thread-safety issues | Use records | | Raw JDBC everywhere | Injection risk, boilerplate | JPA/jOOQ with parameterized queries | | Missing `@Transactional` | Inconsistent data | Annotate service methods | | N+1 queries | Performance death | `JOIN FETCH` or `@EntityGraph` |