Scaffold a production-ready Spring Boot 3.3+ application with Java 21+, Spring Data JPA, Spring Security, Flyway migrations, and comprehensive testing.
77
72%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-java/springboot-project-starter/SKILL.mdScaffold a production-ready Spring Boot 3.3+ application with Java 21+, Spring Data JPA, Spring Security, Flyway migrations, and comprehensive testing.
# Via Spring Initializr CLI (curl)
curl -s https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.3.5 \
-d javaVersion=21 \
-d groupId=com.example \
-d artifactId=myapp \
-d name=myapp \
-d packageName=com.example.myapp \
-d dependencies=web,data-jpa,security,validation,flyway,postgresql,testcontainers,lombok \
-o myapp.zip && unzip myapp.zip -d myapp
# Via Spring Boot CLI (if installed)
spring init --boot-version=3.3.5 --java-version=21 \
--dependencies=web,data-jpa,security,validation,flyway,postgresql,testcontainers,lombok \
--groupId=com.example --artifactId=myapp --name=myapp myapp.zipmyapp/
├── src/
│ ├── main/
│ │ ├── java/com/example/myapp/
│ │ │ ├── MyappApplication.java
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── WebConfig.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ ├── dto/
│ │ │ │ ├── UserRequest.java
│ │ │ │ └── UserResponse.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ ├── exception/
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ └── ResourceNotFoundException.java
│ │ │ ├── mapper/
│ │ │ │ └── UserMapper.java
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java
│ │ │ └── service/
│ │ │ └── UserService.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/migration/
│ │ └── V1__create_users_table.sql
│ └── test/
│ └── java/com/example/myapp/
│ ├── MyappApplicationTests.java
│ ├── controller/
│ │ └── UserControllerTest.java
│ └── service/
│ └── UserServiceTest.java
├── pom.xml
└── docker-compose.ymlUserRequest, UserResponse).@Entity. Never expose entities directly in controllers.@Mapper(componentModel = "spring").@Autowired on fields.@Valid, @NotBlank, @Size, etc.).V{version}__{description}.sql.dev for local development (H2 or Docker Postgres), prod for production.ResponseEntity<T> for explicit HTTP status control.@WebMvcTest for controllers, @DataJpaTest for repositories, @SpringBootTest for integration.package com.example.myapp.entity;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "users")
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;
@Column(nullable = false, updatable = false)
private Instant createdAt;
private Instant updatedAt;
@PrePersist
void onCreate() {
this.createdAt = Instant.now();
}
@PreUpdate
void onUpdate() {
this.updatedAt = Instant.now();
}
// Constructors
protected User() {}
public User(String email, String name) {
this.email = email;
this.name = name;
}
// Getters and setters
public Long getId() { return id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}package com.example.myapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
String email
) {}package com.example.myapp.dto;
import java.time.Instant;
public record UserResponse(
Long id,
String name,
String email,
Instant createdAt
) {}package com.example.myapp.mapper;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring")
public interface UserMapper {
User toEntity(UserRequest request);
UserResponse toResponse(User user);
void updateEntity(UserRequest request, @MappingTarget User user);
}package com.example.myapp.repository;
import com.example.myapp.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}package com.example.myapp.service;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.mapper.UserMapper;
import com.example.myapp.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserService(UserRepository userRepository, UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
@Transactional(readOnly = true)
public List<UserResponse> findAll() {
return userRepository.findAll()
.stream()
.map(userMapper::toResponse)
.toList();
}
@Transactional(readOnly = true)
public UserResponse findById(Long id) {
return userRepository.findById(id)
.map(userMapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}
@Transactional
public UserResponse create(UserRequest request) {
User user = userMapper.toEntity(request);
return userMapper.toResponse(userRepository.save(user));
}
@Transactional
public UserResponse update(Long id, UserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
userMapper.updateEntity(request, user);
return userMapper.toResponse(userRepository.save(user));
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("User", id);
}
userRepository.deleteById(id);
}
}package com.example.myapp.controller;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<UserResponse>> findAll() {
return ResponseEntity.ok(userService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody UserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(
@PathVariable Long id,
@Valid @RequestBody UserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}package com.example.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.build();
}
}package com.example.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.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
fe -> fe.getField(),
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid",
(a, b) -> a
));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Validation failed");
problem.setProperty("errors", errors);
return problem;
}
}-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
);spring:
application:
name: myapp
profiles:
active: dev
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,info,metricsspring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
username: postgres
password: postgres
jpa:
properties:
hibernate:
format_sql: true
logging:
level:
com.example.myapp: DEBUG
org.springframework.security: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACEspring:
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
logging:
level:
com.example.myapp: INFO
org.hibernate.SQL: WARNpackage com.example.myapp.service;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.mapper.UserMapper;
import com.example.myapp.repository.UserRepository;
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.time.Instant;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock UserMapper userMapper;
@InjectMocks UserService userService;
@Test
void findById_existingUser_returnsResponse() {
User user = new User("alice@example.com", "Alice");
UserResponse response = new UserResponse(1L, "Alice", "alice@example.com", Instant.now());
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
when(userMapper.toResponse(user)).thenReturn(response);
UserResponse result = userService.findById(1L);
assertThat(result.name()).isEqualTo("Alice");
verify(userRepository).findById(1L);
}
@Test
void findById_missingUser_throwsNotFound() {
when(userRepository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class);
}
@Test
void create_validRequest_savesAndReturns() {
UserRequest request = new UserRequest("Bob", "bob@example.com");
User entity = new User("bob@example.com", "Bob");
UserResponse response = new UserResponse(2L, "Bob", "bob@example.com", Instant.now());
when(userMapper.toEntity(request)).thenReturn(entity);
when(userRepository.save(entity)).thenReturn(entity);
when(userMapper.toResponse(entity)).thenReturn(response);
UserResponse result = userService.create(request);
assertThat(result.email()).isEqualTo("bob@example.com");
verify(userRepository).save(any(User.class));
}
}package com.example.myapp.controller;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.bean.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.time.Instant;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
@WithMockUser
void findAll_returnsOkWithUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new UserResponse(1L, "Alice", "alice@example.com", Instant.now())
));
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Alice"));
}
}createdb myapp_devsrc/main/resources/application-dev.yml with your database credentials./mvnw compile to generate MapStruct sources and verify the project compiles./mvnw spring-boot:run -Dspring-boot.run.profiles=dev (Flyway runs migrations automatically on startup)curl http://localhost:8080/actuator/health# Run the application (dev profile)
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Run all tests
./mvnw test
# Run a specific test class
./mvnw test -Dtest=UserServiceTest
# Package as JAR
./mvnw clean package -DskipTests
# Run the packaged JAR
java -jar target/myapp-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
# Build Docker image (with Spring Boot's built-in Buildpack support)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest
# Check for dependency updates
./mvnw versions:display-dependency-updates
# Generate MapStruct sources (happens automatically during compile)
./mvnw compileWebMvcConfigurer bean or @CrossOrigin on controllers when pairing with a frontend skill (React, Angular, etc.).springdoc-openapi-starter-webmvc-ui dependency for auto-generated Swagger UI at /swagger-ui.html.micrometer-registry-prometheus for Prometheus metrics and spring-boot-starter-actuator (already included) for health checks.spring-boot:build-image goal produces an OCI image without a Dockerfile. For custom Dockerfiles, use a multi-stage build with Eclipse Temurin as the base../mvnw wrapper ensures reproducible builds without requiring Maven on the CI server.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.