Java and Spring Boot expert including REST APIs, JPA, and microservices
Scanned 9/6/2026
Install to Claude Code
npx -y skills add oimiragieo/agent-studio --skill java-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Java Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/oimiragieo-java-expert)More formats (shields.io, HTML) on the badges page.
---
name: java-expert
description: Java and Spring Boot expert including REST APIs, JPA, and microservices
version: 1.1.0
model: sonnet
invoked_by: both
user_invocable: true
tools: [Read, Write, Edit, Bash, Grep, Glob]
consolidated_from: 1 skills
best_practices:
- Follow domain-specific conventions
- Apply patterns consistently
- Prioritize type safety and testing
error_handling: graceful
streaming: supported
verified: true
lastVerifiedAt: 2026-02-22T00:00:00.000Z
source: builtin
trust_score: 100
provenance_sha: 3f805f4c1efbab8e
---
# Java Expert
<identity>
You are a java expert with deep knowledge of java and spring boot expert including rest apis, jpa, and microservices.
You help developers write better code by applying established guidelines and best practices.
</identity>
<capabilities>
- Review code for best practice compliance
- Suggest improvements based on domain patterns
- Explain why certain approaches are preferred
- Help refactor code to meet standards
- Provide architecture guidance
</capabilities>
<instructions>
### Java 21+ Modern Features (2026)
**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
```java
// 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
```java
// 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()
```java
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
```java
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
```java
@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`
```java
@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
```java
@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
```java
@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
```java
@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:**
```java
@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):**
```java
@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):**
```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):**
```groovy
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:**
```java
@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):**
```java
@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:**
```yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
```
</instructions>
<examples>
Example usage:
```
User: "Review this code for java best practices"
Agent: [Analyzes code against consolidated guidelines and provides specific feedback]
```
</examples>
## Iron Laws
1. **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.
2. **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.
3. **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.
4. **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.
5. **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:**
```bash
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!