| 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+)
public record UserDto(Long id, String name, String email) {
public UserDto {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(email, "email must not be null");
}
}
Sealed Classes (Java 17+)
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+)
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();
};
}
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
var users = new ArrayList<User>();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
List<String> names = users.stream()
.filter(User::isActive)
.map(User::name)
.sorted()
.toList();
Map<Department, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::department));
String city = findByEmail(email)
.map(User::address)
.map(Address::city)
.orElse("Unknown");
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
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
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement(sql);
var rs = stmt.executeQuery()) {
while (rs.next()) { results.add(mapRow(rs)); }
}
Global Handler (Spring)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(ex.errorCode(), ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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
@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
@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<String> invalidEmails() {
return Stream.of("", "no-at-sign", "@no-local", "spaces in@email.com");
}
Mockito Extras
var captor = ArgumentCaptor.forClass(User.class);
verify(repository).save(captor.capture());
assertThat(captor.getValue().name()).isEqualTo("Alice");
given(repository.findById(1L)).willReturn(Optional.of(user));
then(repository).should().findById(1L);
Testcontainers (Integration)
@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)
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public List<UserDto> 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<User, Long> {
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.active = true AND u.role = :role")
List<User> findActiveByRole(@Param("role") Role role);
}
Quarkus
Uses Jakarta REST (@Path, @GET, @POST) + CDI (@Inject, @ApplicationScoped). Panache simplifies JPA: implements PanacheRepository<User> gives find(), persist(), list() out of the box. Use @Transactional on mutating endpoints.
Jackson, Lombok, MapStruct
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record ApiResponse<T>(T data, @JsonInclude(Include.NON_NULL) String error) {}
@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;
}
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User entity);
User toEntity(CreateUserRequest request);
}
Performance Tips
JVM Tuning
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar app.jar
Virtual Threads (Java 21+)
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();
}
Profiling with JFR
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)
<properties>
<java.version>21</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
Gradle Kotlin DSL
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 |