| name | spring-boot-engineer |
| description | Use when implementing full-stack Spring Boot features including web controllers, data access, security, testing, and cloud-native patterns. This is the go-to skill for hands-on Spring Boot development. |
Spring Boot Engineer Skill
You are a senior Spring Boot engineer. You implement features end-to-end, from REST controllers through service layer to data access, following Spring Boot 3.x best practices.
Implementation Workflow
When implementing a new feature:
- Define the API contract - Request/Response DTOs, endpoints, status codes
- Create the entity - JPA entity with proper mapping
- Create the repository - Spring Data JPA with custom queries
- Implement the service - Business logic with transactions
- Build the controller - REST endpoints with validation
- Write tests - Unit, slice, and integration tests
- Add configuration - Properties, profiles, migrations
Complete Feature Example
Entity
@Entity
@Table(name = "products", indexes = {
@Index(name = "idx_products_category", columnList = "category_id"),
@Index(name = "idx_products_sku", columnList = "sku", unique = true)
})
public class Product extends BaseEntity {
@Column(nullable = false, unique = true, length = 50)
private String sku;
@Column(nullable = false, length = 200)
private String name;
@Column(length = 1000)
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ProductStatus status = ProductStatus.ACTIVE;
public Product() {}
public Product(String sku, String name, BigDecimal price, Category category) {
this.sku = sku;
this.name = name;
this.price = price;
this.category = category;
}
public void activate() {
this.status = ProductStatus.ACTIVE;
}
public void discontinue() {
this.status = ProductStatus.DISCONTINUED;
}
public void updatePrice(BigDecimal newPrice) {
if (newPrice.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Price must be positive");
}
this.price = newPrice;
}
public String getSku() { return sku; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getPrice() { return price; }
public Category getCategory() { return category; }
public void setCategory(Category category) { this.category = category; }
public ProductStatus getStatus() { return status; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Product other)) return false;
return Objects.equals(sku, other.sku);
}
@Override
public int hashCode() {
return Objects.hash(sku);
}
}
DTOs
public record CreateProductRequest(
@NotBlank @Size(max = 50) String sku,
@NotBlank @Size(max = 200) String name,
@Size(max = 1000) String description,
@NotNull @Positive BigDecimal price,
@NotNull Long categoryId
) {}
public record UpdateProductRequest(
@NotBlank @Size(max = 200) String name,
@Size(max = 1000) String description,
@NotNull @Positive BigDecimal price,
@NotNull Long categoryId
) {}
public record ProductResponse(
Long id,
String sku,
String name,
String description,
BigDecimal price,
String categoryName,
String status,
LocalDateTime createdAt
) {}
Repository
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
Optional<Product> findBySku(String sku);
boolean existsBySku(String sku);
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.id = :id")
Optional<Product> findByIdWithCategory(@Param("id") Long id);
@Query("SELECT p FROM Product p JOIN FETCH p.category WHERE p.status = :status")
List<Product> findByStatusWithCategory(@Param("status") ProductStatus status);
Page<Product> findByCategoryId(Long categoryId, Pageable pageable);
}
Service
@Service
public class ProductService {
private static final Logger log = LoggerFactory.getLogger(ProductService.class);
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
public ProductService(ProductRepository productRepository,
CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
@Transactional(readOnly = true)
public Page<ProductResponse> findAll(Pageable pageable) {
return productRepository.findAll(pageable)
.map(ProductMapper::toResponse);
}
@Transactional(readOnly = true)
public ProductResponse findById(Long id) {
return productRepository.findByIdWithCategory(id)
.map(ProductMapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@Transactional
public ProductResponse create(CreateProductRequest request) {
if (productRepository.existsBySku(request.sku())) {
throw new DuplicateResourceException("Product with SKU already exists: " + request.sku());
}
var category = categoryRepository.findById(request.categoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category", request.categoryId()));
var product = new Product(request.sku(), request.name(), request.price(), category);
product.setDescription(request.description());
var saved = productRepository.save(product);
log.info("Product created: id={}, sku={}", saved.getId(), saved.getSku());
return ProductMapper.toResponse(saved);
}
@Transactional
public ProductResponse update(Long id, UpdateProductRequest request) {
var product = productRepository.findByIdWithCategory(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
var category = categoryRepository.findById(request.categoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category", request.categoryId()));
product.setName(request.name());
product.setDescription(request.description());
product.updatePrice(request.price());
product.setCategory(category);
var saved = productRepository.save(product);
log.info("Product updated: id={}", saved.getId());
return ProductMapper.toResponse(saved);
}
@Transactional
public void delete(Long id) {
if (!productRepository.existsById(id)) {
throw new ResourceNotFoundException("Product", id);
}
productRepository.deleteById(id);
log.info("Product deleted: id={}", id);
}
}
Controller
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<Page<ProductResponse>> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
var pageable = PageRequest.of(page, size, Sort.by("name"));
return ResponseEntity.ok(productService.findAll(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<ProductResponse> findById(@PathVariable Long id) {
return ResponseEntity.ok(productService.findById(id));
}
@PostMapping
public ResponseEntity<ProductResponse> create(
@Valid @RequestBody CreateProductRequest request) {
var product = productService.create(request);
var location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(product.id()).toUri();
return ResponseEntity.created(location).body(product);
}
@PutMapping("/{id}")
public ResponseEntity<ProductResponse> update(
@PathVariable Long id,
@Valid @RequestBody UpdateProductRequest request) {
return ResponseEntity.ok(productService.update(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}
Reference Materials
See the following reference documents for detailed patterns:
references/web.md - Web layer patterns
references/data.md - Data access patterns
references/security.md - Security implementation
references/testing.md - Testing strategies
references/cloud.md - Cloud-native patterns