Files
James Bland befb8fbaeb feat: initial Claude Code configuration scaffold
Comprehensive Claude Code guidance system with:

- 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit
- 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#),
  infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD),
  testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability)
- 3 hooks: secret detection, auto-formatting, TDD git pre-commit
- Strict TDD enforcement with 80%+ coverage requirements
- Multi-model strategy: Opus for planning, Sonnet for execution (opusplan)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:47:34 -05:00

19 KiB

name, description
name description
java-development Java development patterns with Spring Boot, JUnit 5, and modern Java practices. Use when writing Java code.

Java Development Skill

Project Structure (Spring Boot)

project/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/mycompany/myapp/
│   │   │       ├── MyApplication.java
│   │   │       ├── domain/
│   │   │       │   ├── User.java
│   │   │       │   └── Order.java
│   │   │       ├── repository/
│   │   │       │   └── UserRepository.java
│   │   │       ├── service/
│   │   │       │   └── UserService.java
│   │   │       ├── controller/
│   │   │       │   └── UserController.java
│   │   │       └── config/
│   │   │           └── SecurityConfig.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── application-test.yml
│   └── test/
│       └── java/
│           └── com/mycompany/myapp/
│               ├── service/
│               │   └── UserServiceTest.java
│               └── controller/
│                   └── UserControllerTest.java
├── pom.xml
└── README.md

Testing with JUnit 5

Unit Tests with Mockito

package com.mycompany.myapp.service;

import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
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.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Nested
    @DisplayName("createUser")
    class CreateUser {

        @Test
        @DisplayName("should create user with valid data")
        void shouldCreateUserWithValidData() {
            // Given
            var request = UserFixtures.createUserRequest();
            var savedUser = UserFixtures.user();
            when(userRepository.save(any(User.class))).thenReturn(savedUser);

            // When
            var result = userService.createUser(request);

            // Then
            assertThat(result.getId()).isNotNull();
            assertThat(result.getEmail()).isEqualTo(request.getEmail());
            verify(userRepository).save(any(User.class));
        }

        @Test
        @DisplayName("should throw exception for duplicate email")
        void shouldThrowExceptionForDuplicateEmail() {
            // Given
            var request = UserFixtures.createUserRequest();
            when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);

            // When/Then
            assertThatThrownBy(() -> userService.createUser(request))
                .isInstanceOf(DuplicateEmailException.class)
                .hasMessageContaining(request.getEmail());
        }
    }

    @Nested
    @DisplayName("getUserById")
    class GetUserById {

        @Test
        @DisplayName("should return user when found")
        void shouldReturnUserWhenFound() {
            // Given
            var user = UserFixtures.user();
            when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));

            // When
            var result = userService.getUserById(user.getId());

            // Then
            assertThat(result).isPresent();
            assertThat(result.get().getId()).isEqualTo(user.getId());
        }

        @Test
        @DisplayName("should return empty when user not found")
        void shouldReturnEmptyWhenNotFound() {
            // Given
            when(userRepository.findById(any())).thenReturn(Optional.empty());

            // When
            var result = userService.getUserById("unknown-id");

            // Then
            assertThat(result).isEmpty();
        }
    }
}

Test Fixtures

package com.mycompany.myapp.fixtures;

import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.dto.CreateUserRequest;

import java.time.Instant;
import java.util.UUID;

public final class UserFixtures {

    private UserFixtures() {}

    public static User user() {
        return user(builder -> {});
    }

    public static User user(UserCustomizer customizer) {
        var builder = User.builder()
            .id(UUID.randomUUID().toString())
            .email("test@example.com")
            .name("Test User")
            .role(Role.USER)
            .createdAt(Instant.now())
            .updatedAt(Instant.now());

        customizer.customize(builder);
        return builder.build();
    }

    public static CreateUserRequest createUserRequest() {
        return createUserRequest(builder -> {});
    }

    public static CreateUserRequest createUserRequest(CreateUserRequestCustomizer customizer) {
        var builder = CreateUserRequest.builder()
            .email("newuser@example.com")
            .name("New User")
            .password("SecurePass123!");

        customizer.customize(builder);
        return builder.build();
    }

    @FunctionalInterface
    public interface UserCustomizer {
        void customize(User.UserBuilder builder);
    }

    @FunctionalInterface
    public interface CreateUserRequestCustomizer {
        void customize(CreateUserRequest.CreateUserRequestBuilder builder);
    }
}

// Usage in tests
var adminUser = UserFixtures.user(builder -> builder.role(Role.ADMIN));
var customRequest = UserFixtures.createUserRequest(builder ->
    builder.email("custom@example.com"));

Parameterized Tests

package com.mycompany.myapp.validation;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.*;

class EmailValidatorTest {

    private final EmailValidator validator = new EmailValidator();

    @ParameterizedTest
    @DisplayName("should accept valid emails")
    @ValueSource(strings = {
        "user@example.com",
        "user.name@example.com",
        "user+tag@example.co.uk"
    })
    void shouldAcceptValidEmails(String email) {
        assertThat(validator.isValid(email)).isTrue();
    }

    @ParameterizedTest
    @DisplayName("should reject invalid emails")
    @ValueSource(strings = {
        "invalid",
        "missing@domain",
        "@nodomain.com",
        "spaces in@email.com"
    })
    void shouldRejectInvalidEmails(String email) {
        assertThat(validator.isValid(email)).isFalse();
    }

    @ParameterizedTest
    @DisplayName("should reject null and empty emails")
    @NullAndEmptySource
    void shouldRejectNullAndEmpty(String email) {
        assertThat(validator.isValid(email)).isFalse();
    }

    @ParameterizedTest
    @DisplayName("should validate password strength")
    @CsvSource({
        "password,false",
        "Password1,false",
        "Password1!,true",
        "P@ssw0rd,true"
    })
    void shouldValidatePasswordStrength(String password, boolean expected) {
        assertThat(validator.isStrongPassword(password)).isEqualTo(expected);
    }
}

Integration Tests with Spring

package com.mycompany.myapp.controller;

import com.mycompany.myapp.fixtures.UserFixtures;
import com.mycompany.myapp.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.context.ActiveProfiles;
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
@ActiveProfiles("test")
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }

    @Test
    @DisplayName("POST /api/users creates user and returns 201")
    void createUserReturnsCreated() throws Exception {
        var request = """
            {
                "email": "new@example.com",
                "name": "New User",
                "password": "SecurePass123!"
            }
            """;

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(request))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").isNotEmpty())
            .andExpect(jsonPath("$.email").value("new@example.com"))
            .andExpect(jsonPath("$.name").value("New User"));
    }

    @Test
    @DisplayName("GET /api/users/{id} returns user when found")
    void getUserReturnsUserWhenFound() throws Exception {
        var user = userRepository.save(UserFixtures.user());

        mockMvc.perform(get("/api/users/{id}", user.getId()))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(user.getId()))
            .andExpect(jsonPath("$.email").value(user.getEmail()));
    }

    @Test
    @DisplayName("GET /api/users/{id} returns 404 when not found")
    void getUserReturns404WhenNotFound() throws Exception {
        mockMvc.perform(get("/api/users/{id}", "unknown-id"))
            .andExpect(status().isNotFound());
    }
}

Domain Models with Records

package com.mycompany.myapp.domain;

import jakarta.validation.constraints.*;
import java.time.Instant;

// Immutable domain model using records (Java 17+)
public record User(
    String id,
    @NotBlank @Email String email,
    @NotBlank @Size(max = 100) String name,
    Role role,
    Instant createdAt,
    Instant updatedAt
) {
    // Compact constructor for validation
    public User {
        if (email != null) {
            email = email.toLowerCase().trim();
        }
    }

    // Static factory methods
    public static User create(String email, String name) {
        var now = Instant.now();
        return new User(
            UUID.randomUUID().toString(),
            email,
            name,
            Role.USER,
            now,
            now
        );
    }

    // Wither methods for immutable updates
    public User withName(String newName) {
        return new User(id, email, newName, role, createdAt, Instant.now());
    }

    public User withRole(Role newRole) {
        return new User(id, email, name, newRole, createdAt, Instant.now());
    }
}

// For mutable entities (JPA)
@Entity
@Table(name = "users")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String name;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Column(name = "created_at")
    private Instant createdAt;

    @Column(name = "updated_at")
    private Instant updatedAt;

    @PrePersist
    void onCreate() {
        createdAt = Instant.now();
        updatedAt = createdAt;
    }

    @PreUpdate
    void onUpdate() {
        updatedAt = Instant.now();
    }
}

Service Layer

package com.mycompany.myapp.service;

import com.mycompany.myapp.domain.User;
import com.mycompany.myapp.dto.CreateUserRequest;
import com.mycompany.myapp.exception.DuplicateEmailException;
import com.mycompany.myapp.exception.UserNotFoundException;
import com.mycompany.myapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public User createUser(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException(request.getEmail());
        }

        var user = User.create(
            request.getEmail(),
            request.getName()
        );

        return userRepository.save(user);
    }

    public Optional<User> getUserById(String id) {
        return userRepository.findById(id);
    }

    public User getUserByIdOrThrow(String id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    @Transactional
    public User updateUser(String id, UpdateUserRequest request) {
        var user = getUserByIdOrThrow(id);

        var updatedUser = user
            .withName(request.getName())
            .withRole(request.getRole());

        return userRepository.save(updatedUser);
    }

    @Transactional
    public void deleteUser(String id) {
        if (!userRepository.existsById(id)) {
            throw new UserNotFoundException(id);
        }
        userRepository.deleteById(id);
    }
}

REST Controllers

package com.mycompany.myapp.controller;

import com.mycompany.myapp.dto.*;
import com.mycompany.myapp.service.UserService;
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;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final UserMapper userMapper;

    @GetMapping
    public List<UserResponse> listUsers() {
        return userService.getAllUsers().stream()
            .map(userMapper::toResponse)
            .toList();
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable String id) {
        return userService.getUserById(id)
            .map(userMapper::toResponse)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
        var user = userService.createUser(request);
        return userMapper.toResponse(user);
    }

    @PutMapping("/{id}")
    public UserResponse updateUser(
            @PathVariable String id,
            @Valid @RequestBody UpdateUserRequest request) {
        var user = userService.updateUser(id, request);
        return userMapper.toResponse(user);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable String id) {
        userService.deleteUser(id);
    }
}

Exception Handling

package com.mycompany.myapp.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.net.URI;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
        var problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            ex.getMessage()
        );
        problem.setTitle("User Not Found");
        problem.setType(URI.create("https://api.myapp.com/errors/user-not-found"));
        return problem;
    }

    @ExceptionHandler(DuplicateEmailException.class)
    public ProblemDetail handleDuplicateEmail(DuplicateEmailException ex) {
        var problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.CONFLICT,
            ex.getMessage()
        );
        problem.setTitle("Duplicate Email");
        problem.setType(URI.create("https://api.myapp.com/errors/duplicate-email"));
        return problem;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        var problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST,
            "Validation failed"
        );
        problem.setTitle("Validation Error");

        var errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> new ValidationError(e.getField(), e.getDefaultMessage()))
            .toList();

        problem.setProperty("errors", errors);
        return problem;
    }

    record ValidationError(String field, String message) {}
}

Commands

# Maven
mvn clean install                      # Build and test
mvn test                               # Run tests only
mvn verify                             # Run integration tests
mvn spring-boot:run                    # Run application
mvn jacoco:report                      # Generate coverage report

# Gradle
./gradlew build                        # Build and test
./gradlew test                         # Run tests only
./gradlew integrationTest              # Run integration tests
./gradlew bootRun                      # Run application
./gradlew jacocoTestReport             # Generate coverage report

# Common flags
-DskipTests                            # Skip tests
-Dspring.profiles.active=test          # Set profile

Dependencies (pom.xml)

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- Testing -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>