home / skills / doanchienthangdev / omgkit / spring

This skill helps you design enterprise Spring Boot applications with JPA, security, REST APIs, and microservices patterns for scalable Java solutions.

npx playbooks add skill doanchienthangdev/omgkit --skill spring

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

Files (1)
SKILL.md
16.1 KB
---
name: spring
description: Enterprise Spring Boot development with JPA, security, testing, and microservices patterns
category: frameworks
triggers:
  - spring
  - spring boot
  - java spring
  - spring mvc
  - spring security
  - jpa
  - hibernate
  - java api
---

# Spring Boot

Enterprise-grade **Spring Boot development** following industry best practices. This skill covers Spring Data JPA, Spring Security, REST APIs, validation, testing patterns, and microservices configurations used by top engineering teams.

## Purpose

Build scalable Java applications with confidence:

- Design clean architectures with Spring Boot
- Implement REST APIs with proper validation
- Use Spring Data JPA for database operations
- Handle authentication with Spring Security
- Write comprehensive tests with JUnit and MockMvc
- Deploy production-ready applications
- Build microservices with Spring Cloud

## Features

### 1. Entity Design and Relationships

```java
// src/main/java/com/example/model/User.java
package com.example.model;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.annotations.UuidGenerator;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class User {
    @Id
    @UuidGenerator
    private UUID id;

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    @Builder.Default
    private UserRole role = UserRole.USER;

    @Builder.Default
    private Boolean isActive = true;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    @Builder.Default
    private Set<Membership> memberships = new HashSet<>();

    public boolean isAdmin() {
        return this.role == UserRole.ADMIN;
    }
}


// src/main/java/com/example/model/Organization.java
@Entity
@Table(name = "organizations")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Organization {
    @Id
    @UuidGenerator
    private UUID id;

    @Column(nullable = false)
    private String name;

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

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

    @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
    @Builder.Default
    private Set<Membership> memberships = new HashSet<>();

    @CreationTimestamp
    private LocalDateTime createdAt;
}
```

### 2. DTOs and Validation

```java
// src/main/java/com/example/dto/user/CreateUserRequest.java
package com.example.dto.user;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class CreateUserRequest {
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100)
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;

    @NotBlank(message = "Password is required")
    @Size(min = 8, max = 128)
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).*$",
             message = "Password must contain uppercase, lowercase, number and special character")
    private String password;

    private String role;
}


// src/main/java/com/example/dto/user/UserResponse.java
@Data
@Builder
public class UserResponse {
    private UUID id;
    private String email;
    private String name;
    private UserRole role;
    private Boolean isActive;
    private LocalDateTime createdAt;

    public static UserResponse fromEntity(User user) {
        return UserResponse.builder()
            .id(user.getId())
            .email(user.getEmail())
            .name(user.getName())
            .role(user.getRole())
            .isActive(user.getIsActive())
            .createdAt(user.getCreatedAt())
            .build();
    }
}


// src/main/java/com/example/dto/common/PaginatedResponse.java
@Data
@Builder
public class PaginatedResponse<T> {
    private List<T> data;
    private int page;
    private int limit;
    private long total;
    private int totalPages;
    private boolean hasMore;

    public static <T, E> PaginatedResponse<T> fromPage(
        Page<E> page,
        java.util.function.Function<E, T> mapper
    ) {
        return PaginatedResponse.<T>builder()
            .data(page.getContent().stream().map(mapper).toList())
            .page(page.getNumber() + 1)
            .limit(page.getSize())
            .total(page.getTotalElements())
            .totalPages(page.getTotalPages())
            .hasMore(page.hasNext())
            .build();
    }
}
```

### 3. Repositories

```java
// src/main/java/com/example/repository/UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
    Optional<User> findByEmail(String email);

    boolean existsByEmail(String email);

    @Query("""
        SELECT u FROM User u
        WHERE u.isActive = true
        AND (:search IS NULL OR LOWER(u.name) LIKE LOWER(CONCAT('%', :search, '%'))
             OR LOWER(u.email) LIKE LOWER(CONCAT('%', :search, '%')))
        AND (:role IS NULL OR u.role = :role)
        """)
    Page<User> findAllWithFilters(
        @Param("search") String search,
        @Param("role") UserRole role,
        Pageable pageable
    );
}
```

### 4. Services

```java
// src/main/java/com/example/service/UserService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public PaginatedResponse<UserResponse> findAll(String search, String role, int page, int limit) {
        UserRole userRole = role != null ? UserRole.valueOf(role.toUpperCase()) : null;
        PageRequest pageRequest = PageRequest.of(page - 1, limit, Sort.by("createdAt").descending());

        Page<User> users = userRepository.findAllWithFilters(search, userRole, pageRequest);

        return PaginatedResponse.fromPage(users, UserResponse::fromEntity);
    }

    public UserResponse findById(UUID id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found with id: " + id));
        return UserResponse.fromEntity(user);
    }

    @Transactional
    public UserResponse create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new ConflictException("Email already in use");
        }

        User user = User.builder()
            .email(request.getEmail().toLowerCase())
            .name(request.getName())
            .password(passwordEncoder.encode(request.getPassword()))
            .role(request.getRole() != null ? UserRole.valueOf(request.getRole().toUpperCase()) : UserRole.USER)
            .build();

        return UserResponse.fromEntity(userRepository.save(user));
    }

    @Transactional
    public UserResponse update(UUID id, UpdateUserRequest request) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found"));

        if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
            if (userRepository.existsByEmail(request.getEmail())) {
                throw new ConflictException("Email already in use");
            }
            user.setEmail(request.getEmail().toLowerCase());
        }

        if (request.getName() != null) user.setName(request.getName());
        if (request.getIsActive() != null) user.setIsActive(request.getIsActive());

        return UserResponse.fromEntity(userRepository.save(user));
    }

    @Transactional
    public void delete(UUID id) {
        if (!userRepository.existsById(id)) {
            throw new NotFoundException("User not found");
        }
        userRepository.deleteById(id);
    }
}
```

### 5. Controllers

```java
// src/main/java/com/example/controller/UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "Users", description = "User management endpoints")
public class UserController {
    private final UserService userService;

    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")
    @Operation(summary = "List all users")
    public ResponseEntity<PaginatedResponse<UserResponse>> findAll(
        @RequestParam(required = false) String search,
        @RequestParam(required = false) String role,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "20") int limit
    ) {
        return ResponseEntity.ok(userService.findAll(search, role, page, limit));
    }

    @GetMapping("/me")
    @Operation(summary = "Get current user profile")
    public ResponseEntity<UserResponse> getCurrentUser(@AuthenticationPrincipal User currentUser) {
        return ResponseEntity.ok(userService.findById(currentUser.getId()));
    }

    @GetMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<UserResponse> findById(@PathVariable UUID id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
    }

    @PatchMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<UserResponse> update(@PathVariable UUID id, @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable UUID id) {
        userService.delete(id);
    }
}
```

### 6. Security Configuration

```java
// src/main/java/com/example/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthFilter;

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

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```

### 7. Testing

```java
// src/test/java/com/example/service/UserServiceTest.java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @InjectMocks
    private UserService userService;

    private User testUser;

    @BeforeEach
    void setUp() {
        testUser = User.builder()
            .id(UUID.randomUUID())
            .email("[email protected]")
            .name("Test User")
            .role(UserRole.USER)
            .isActive(true)
            .build();
    }

    @Test
    void findById_ShouldReturnUser_WhenUserExists() {
        when(userRepository.findById(testUser.getId())).thenReturn(Optional.of(testUser));

        UserResponse result = userService.findById(testUser.getId());

        assertThat(result.getEmail()).isEqualTo(testUser.getEmail());
    }

    @Test
    void findById_ShouldThrowNotFoundException_WhenUserNotFound() {
        UUID id = UUID.randomUUID();
        when(userRepository.findById(id)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.findById(id))
            .isInstanceOf(NotFoundException.class);
    }

    @Test
    void create_ShouldCreateUser_WhenEmailIsUnique() {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("[email protected]");
        request.setName("New User");
        request.setPassword("Password123!");

        when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
        when(passwordEncoder.encode(request.getPassword())).thenReturn("encoded");
        when(userRepository.save(any(User.class))).thenReturn(testUser);

        UserResponse result = userService.create(request);

        assertThat(result).isNotNull();
        verify(userRepository).save(any(User.class));
    }

    @Test
    void create_ShouldThrowConflictException_WhenEmailExists() {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("[email protected]");

        when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);

        assertThatThrownBy(() -> userService.create(request))
            .isInstanceOf(ConflictException.class);
    }
}


// src/test/java/com/example/controller/UserControllerTest.java
@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private UserService userService;

    @Test
    @WithMockUser(roles = "ADMIN")
    void create_ShouldReturn201_WhenValidRequest() throws Exception {
        CreateUserRequest request = new CreateUserRequest();
        request.setEmail("[email protected]");
        request.setName("Test User");
        request.setPassword("Password123!");

        UserResponse response = UserResponse.builder()
            .email("[email protected]")
            .name("Test User")
            .build();

        when(userService.create(any())).thenReturn(response);

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

    @Test
    @WithMockUser(roles = "USER")
    void findAll_ShouldReturn403_WhenNotAdmin() throws Exception {
        mockMvc.perform(get("/api/v1/users"))
            .andExpect(status().isForbidden());
    }
}
```

## Use Cases

### Caching with Redis

```java
@Service
@RequiredArgsConstructor
public class CachedUserService {
    private final UserRepository userRepository;

    @Cacheable(value = "users", key = "#id")
    public UserResponse findById(UUID id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found"));
        return UserResponse.fromEntity(user);
    }

    @CacheEvict(value = "users", key = "#id")
    public void evictCache(UUID id) {}
}
```

## Best Practices

### Do's

- Use UUID primary keys for public APIs
- Use DTOs for request/response separation
- Use Spring Data JPA specifications for complex queries
- Use @Transactional appropriately
- Use proper validation annotations
- Write unit and integration tests
- Use Spring Security for authentication
- Use proper exception handling
- Use constructor injection
- Document APIs with OpenAPI

### Don'ts

- Don't expose entities directly in APIs
- Don't use field injection
- Don't ignore N+1 query problems
- Don't skip validation
- Don't hardcode configuration
- Don't ignore security headers
- Don't skip error handling
- Don't use raw SQL without parameterization
- Don't forget to handle exceptions globally
- Don't skip testing

## References

- [Spring Boot Documentation](https://spring.io/projects/spring-boot)
- [Spring Data JPA](https://spring.io/projects/spring-data-jpa)
- [Spring Security](https://spring.io/projects/spring-security)
- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
- [Baeldung Spring Tutorials](https://www.baeldung.com/spring-boot)

Overview

This skill provides enterprise-grade Spring Boot development patterns for building scalable Java applications. It covers entity modeling, DTO validation, JPA repositories, service layering, REST controllers, security configuration, and testing strategies used in production systems.

How this skill works

The skill inspects and demonstrates common Spring Boot building blocks: JPA entity design with relationships and timestamps, DTOs with Jakarta Validation, repository queries and pagination, service methods with transactional boundaries and error handling, controller endpoints with role-based access, JWT-based security configuration, and unit/web tests using JUnit, Mockito, and MockMvc. It includes concrete code examples for each layer and promotes patterns for maintainability and safety.

When to use it

  • Starting a new Spring Boot microservice that needs secure user management and persistence
  • Implementing REST APIs with consistent validation, pagination, and mapping to DTOs
  • Adding role-based access control and stateless JWT authentication
  • Designing domain models with clear relationships and lifecycle timestamps
  • Establishing automated unit and controller tests to prevent regressions

Best practices

  • Keep services thin: validate input in controllers/DTOs and encapsulate business logic in services
  • Use DTOs for external APIs and map entities to responses to avoid leaking internals
  • Define repository query methods with filters and pagination for performant list endpoints
  • Protect endpoints with method-level security and a stateless JWT filter
  • Write focused unit tests for services and slice tests for controllers with MockMvc

Example use cases

  • User management microservice with create, update, soft-delete, and paginated listing
  • Organization and membership management with owner relationships and cascade behavior
  • Admin-only endpoints for audit and user lifecycle operations
  • Integrating Spring Security with JWT for single-sign-on or API-only services
  • CI pipeline tests that run unit tests and controller slice tests before deployment

FAQ

How does pagination and filtering work in list endpoints?

Repositories expose a Page<T> query with dynamic filters; services build PageRequest and map entities to DTOs using a PaginatedResponse wrapper that includes page, limit, total, and hasMore.

How are passwords handled securely?

Passwords are encoded with a PasswordEncoder (BCrypt) at service creation time and never returned in responses; DTOs exclude sensitive fields.