home / skills / personamanagmentlayer / pcl / spring-boot-expert

spring-boot-expert skill

/stdlib/frameworks/spring-boot-expert

This skill provides expert Spring Boot guidance for building REST APIs and microservices with secure, scalable patterns.

npx playbooks add skill personamanagmentlayer/pcl --skill spring-boot-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
17.4 KB
---
name: spring-boot-expert
version: 1.0.0
description: Expert-level Spring Boot, Spring Framework, REST APIs, and microservices development
category: frameworks
tags: [spring-boot, java, spring-framework, rest-api, microservices]
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash(mvn:*, gradle:*, java:*)
---

# Spring Boot Expert

Expert guidance for Spring Boot development, Spring Framework, building REST APIs, and microservices architecture.

## Core Concepts

### Spring Boot Fundamentals
- Auto-configuration
- Dependency injection
- Spring Boot Starters
- Application properties
- Profiles and configuration
- Spring Boot Actuator

### Spring Framework
- Spring Core (IoC, DI)
- Spring Data JPA
- Spring Security
- Spring Web MVC
- Spring AOP
- Spring Transaction Management

### Microservices
- Service discovery
- API Gateway
- Circuit breakers
- Distributed tracing
- Configuration management

## Spring Boot Application

```java
// Main application class
@SpringBootApplication
@EnableJpaAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// Entity with JPA
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false)
    private String password;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Post> posts = new ArrayList<>();

    // Getters and setters
}

@Entity
@Table(name = "posts")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User author;

    @CreatedDate
    private LocalDateTime createdAt;

    // Getters and setters
}
```

## REST API Controller

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

    private final UserService userService;

    @GetMapping
    public ResponseEntity<Page<UserDto>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "ASC") Sort.Direction direction
    ) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
        Page<UserDto> users = userService.findAll(pageable);
        return ResponseEntity.ok(users);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        return userService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<UserDto> createUser(
            @Valid @RequestBody UserCreateDto userDto
    ) {
        UserDto created = userService.create(userDto);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(created.getId())
                .toUri();

        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserDto> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateDto userDto
    ) {
        return userService.update(id, userDto)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

// DTOs with validation
public record UserDto(
        Long id,
        String email,
        LocalDateTime createdAt
) {}

public record UserCreateDto(
        @NotBlank @Email String email,
        @NotBlank @Size(min = 8) String password
) {}

public record UserUpdateDto(
        @Email String email
) {}
```

## Service Layer

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

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final UserMapper userMapper;

    public Page<UserDto> findAll(Pageable pageable) {
        return userRepository.findAll(pageable)
                .map(userMapper::toDto);
    }

    public Optional<UserDto> findById(Long id) {
        return userRepository.findById(id)
                .map(userMapper::toDto);
    }

    public Optional<UserDto> findByEmail(String email) {
        return userRepository.findByEmail(email)
                .map(userMapper::toDto);
    }

    @Transactional
    public UserDto create(UserCreateDto dto) {
        if (userRepository.existsByEmail(dto.email())) {
            throw new DuplicateEmailException("Email already exists");
        }

        User user = new User();
        user.setEmail(dto.email());
        user.setPassword(passwordEncoder.encode(dto.password()));

        User saved = userRepository.save(user);
        return userMapper.toDto(saved);
    }

    @Transactional
    public Optional<UserDto> update(Long id, UserUpdateDto dto) {
        return userRepository.findById(id)
                .map(user -> {
                    if (dto.email() != null) {
                        user.setEmail(dto.email());
                    }
                    return userMapper.toDto(user);
                });
    }

    @Transactional
    public boolean delete(Long id) {
        if (userRepository.existsById(id)) {
            userRepository.deleteById(id);
            return true;
        }
        return false;
    }
}

// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);

    @Query("SELECT u FROM User u WHERE u.createdAt > :date")
    List<User> findRecentUsers(@Param("date") LocalDateTime date);
}

// Mapper with MapStruct
@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDto toDto(User user);
    User toEntity(UserCreateDto dto);
}
```

## Spring Security with JWT

```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll()
                        .requestMatchers("/api/public/**").permitAll()
                        .requestMatchers("/actuator/**").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authenticationProvider(authenticationProvider)
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userEmail = jwtService.extractUsername(jwt);

        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

@Service
@RequiredArgsConstructor
public class JwtService {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long jwtExpiration;

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public String generateToken(UserDetails userDetails) {
        return buildToken(new HashMap<>(), userDetails, jwtExpiration);
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private String buildToken(
            Map<String, Object> extraClaims,
            UserDetails userDetails,
            long expiration
    ) {
        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts
                .parserBuilder()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}
```

## Exception Handling

```java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
                "NOT_FOUND",
                ex.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(DuplicateEmailException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateEmail(DuplicateEmailException ex) {
        ErrorResponse error = new ErrorResponse(
                "DUPLICATE_EMAIL",
                ex.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidation(
            MethodArgumentNotValidException ex
    ) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
                errors.put(error.getField(), error.getDefaultMessage())
        );

        ValidationErrorResponse response = new ValidationErrorResponse(
                "VALIDATION_ERROR",
                "Request validation failed",
                errors,
                LocalDateTime.now()
        );

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
        ErrorResponse error = new ErrorResponse(
                "INTERNAL_ERROR",
                "An unexpected error occurred",
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

public record ErrorResponse(
        String code,
        String message,
        LocalDateTime timestamp
) {}

public record ValidationErrorResponse(
        String code,
        String message,
        Map<String, String> errors,
        LocalDateTime timestamp
) {}
```

## Configuration

```yaml
# application.yml
spring:
  application:
    name: user-service

  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:postgres}
    driver-class-name: org.postgresql.Driver

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.PostgreSQLDialect

  flyway:
    enabled: true
    baseline-on-migrate: true

server:
  port: 8080
  error:
    include-message: always
    include-binding-errors: always

jwt:
  secret: ${JWT_SECRET:your-secret-key-here}
  expiration: 3600000  # 1 hour

logging:
  level:
    root: INFO
    com.example: DEBUG
```

## Testing

```java
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

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

    @Test
    void shouldCreateUser() throws Exception {
        UserCreateDto dto = new UserCreateDto("[email protected]", "password123");

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.email").value("[email protected]"));
    }

    @Test
    void shouldGetUser() throws Exception {
        User user = createTestUser();

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

    @Test
    void shouldReturnNotFoundForInvalidId() throws Exception {
        mockMvc.perform(get("/api/users/999"))
                .andExpect(status().isNotFound());
    }

    private User createTestUser() {
        User user = new User();
        user.setEmail("[email protected]");
        user.setPassword("hashed-password");
        return userRepository.save(user);
    }
}

// Service unit test
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldCreateUser() {
        UserCreateDto dto = new UserCreateDto("[email protected]", "password123");
        User user = new User();
        UserDto expected = new UserDto(1L, "[email protected]", LocalDateTime.now());

        when(userRepository.existsByEmail(dto.email())).thenReturn(false);
        when(passwordEncoder.encode(dto.password())).thenReturn("hashed");
        when(userRepository.save(any(User.class))).thenReturn(user);
        when(userMapper.toDto(user)).thenReturn(expected);

        UserDto result = userService.create(dto);

        assertNotNull(result);
        assertEquals(expected.email(), result.email());
        verify(userRepository).save(any(User.class));
    }
}
```

## Best Practices

- Use constructor injection
- Separate concerns (Controller/Service/Repository)
- Implement proper exception handling
- Use DTOs for API layer
- Write comprehensive tests
- Use database migrations (Flyway/Liquibase)
- Implement security properly
- Use profiles for different environments
- Enable Spring Boot Actuator for monitoring
- Use connection pooling
- Implement caching where appropriate
- Follow RESTful conventions

## Anti-Patterns

❌ Field injection
❌ Business logic in controllers
❌ No exception handling
❌ Exposing entities directly
❌ Hardcoded configuration
❌ No transaction management
❌ Missing validation

## Resources

- Spring Boot Documentation: https://spring.io/projects/spring-boot
- Spring Framework: https://spring.io/projects/spring-framework
- Spring Data JPA: https://spring.io/projects/spring-data-jpa
- Spring Security: https://spring.io/projects/spring-security
- Baeldung: https://www.baeldung.com/

Overview

This skill provides expert-level guidance for building production-quality Spring Boot applications, REST APIs, and microservices using the Spring Framework. It focuses on reliable architecture, secure authentication with JWT, robust service layers, and practical patterns for data access, validation, and error handling. The content is centered on pragmatic code patterns, configuration advice, and operational considerations for scalable services.

How this skill works

The skill inspects common Spring Boot building blocks: application bootstrap, JPA entities and repositories, service-layer transactions, DTO mapping, REST controllers with pagination and validation, JWT-based security filters, and centralized exception handling. It outlines configuration best practices (application.yml), actuator and monitoring hooks, and microservice concerns like service discovery and API gateway patterns. Concrete examples illustrate request/response flows, transaction boundaries, and secure token processing.

When to use it

  • Building a new Spring Boot REST service with JPA-backed persistence and DTO mapping.
  • Designing secure APIs with stateless JWT authentication and method-level security.
  • Refactoring monolith endpoints into microservices with service discovery and gateways.
  • Implementing consistent error handling, validation responses, and API pagination.
  • Hardening production deployments with Actuator, Flyway migrations, and environment profiles.

Best practices

  • Keep controllers thin: delegate business logic to transactional service classes and return DTOs.
  • Use MapStruct or similar mappers to separate entity and API models and avoid leaking persistence internals.
  • Secure endpoints with stateless JWT filters and an AuthenticationProvider; restrict actuator to admin roles.
  • Validate inputs with javax validation annotations and return structured validation error responses.
  • Manage database migrations with Flyway and set hibernate.ddl-auto appropriately for environments.
  • Prefer explicit pagination and sorting on list endpoints to avoid loading large result sets.

Example use cases

  • User management service with CRUD endpoints, paging, and email-unique constraints enforced at the service layer.
  • Authentication microservice issuing JWTs and an API gateway routing to downstream services with circuit breakers.
  • Content service where User and Post entities are mapped to compact DTOs and audited timestamps are recorded.
  • Migration of legacy endpoints: introduce service-layer transactions and centralized exception handling for consistent responses.
  • Production readiness checklist: secure secrets with env vars, enable Actuator with restricted access, and configure logging levels.

FAQ

How should I store secrets like JWT keys and DB passwords?

Keep secrets out of source control: use environment variables, a secrets manager, or a vault. Inject them via configuration placeholders and avoid embedding defaults in code.

When should I use @Transactional on service methods?

Annotate service methods that perform mutations or need consistent read/write semantics. Use readOnly=true for query-only methods to optimize performance.