
Spring Boot Development
- 395 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
spring-boot-development is an agent skill that guides developers to build Spring Boot REST services with auto-configuration, constructor injection, Spring Data JPA, and Spring Security for JVM enterprise APIs.
About
spring-boot-development is a version 1.0.0 skill from manutej/luxor-claude-marketplace's luxor-backend-toolkit plugin that teaches idiomatic Spring Boot application structure backed by official Spring documentation via Context7. The skill covers auto-configuration mechanics and conditional beans, constructor-based dependency injection over field injection, REST controller patterns with ResponseEntity status handling, Spring Data JPA repositories, application.properties and profile-based configuration, and Spring Security setup for production services. Developers reach for spring-boot-development when scaffolding enterprise SaaS backends, migrating legacy Java applications, or implementing secure database-driven microservices with transactional service layers and DTO patterns. The marketplace bundles 67+ production-grade skills across backend, frontend, and DevOps plugins, and this skill installs via `npx skills add manutej/luxor-claude-marketplace --skill spring-boot-development`.
- REST controller design
- Dependency injection setup
- JPA and data access layers
- Configuration and profiles
- Security and middleware integration
Spring Boot Development by the numbers
- 395 all-time installs (skills.sh)
- +20 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #16 of 89 Java & JVM skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill spring-boot-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 395 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
How do you build a Spring Boot REST API?
Build Spring Boot REST services, configuration, persistence, security, and dependency injection layers when creating JVM-backed APIs and enterprise SaaS backends in Java.
Who is it for?
Java developers building enterprise REST microservices or SaaS backends who want idiomatic Spring Boot patterns with JPA and security.
Skip if: Teams building Kotlin-only Ktor services or frontend React applications without a JVM Spring Boot backend layer.
When should I use this skill?
The user asks to create a Spring Boot REST API, configure Spring Data JPA, set up Spring Security, or implement dependency injection and auto-configuration in Java.
What you get
Spring Boot project structure, REST controllers, JPA entity and repository layers, security configuration, and application profile property files.
- REST controller code
- JPA entity layers
- Spring Security configuration
By the numbers
- Skill version 1.0.0 in luxor-claude-marketplace
- LUXOR marketplace bundles 67+ production-grade skills
- luxor-backend-toolkit plugin includes 14 backend skills
Files
Spring Boot Development Skill
This skill provides comprehensive guidance for building modern Spring Boot applications using auto-configuration, dependency injection, REST APIs, Spring Data, Spring Security, and enterprise Java patterns based on official Spring Boot documentation.
When to Use This Skill
Use this skill when:
- Building enterprise REST APIs and microservices
- Creating web applications with Spring MVC
- Developing data-driven applications with JPA and databases
- Implementing authentication and authorization with Spring Security
- Building production-ready applications with actuator and monitoring
- Creating scalable backend services with Spring Boot
- Migrating from traditional Spring to Spring Boot
- Developing cloud-native applications
- Building event-driven systems with messaging
- Creating batch processing applications
Core Concepts
Auto-Configuration
Spring Boot automatically configures your application based on the dependencies you have added to the project. This reduces boilerplate configuration significantly.
How Auto-Configuration Works:
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}The @SpringBootApplication annotation is a combination of:
@Configuration: Tags the class as a source of bean definitions@EnableAutoConfiguration: Enables Spring Boot's auto-configuration mechanism@ComponentScan: Enables component scanning in the current package and sub-packages
Conditional Auto-Configuration:
@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.url")
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}Customizing Auto-Configuration:
// Exclude specific auto-configurations
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication {
// ...
}
// Or in application.properties
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationDependency Injection
Spring's IoC (Inversion of Control) container manages object creation and dependency injection.
Constructor Injection (Recommended):
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Constructor injection - recommended approach
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(User user) {
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(saved);
return saved;
}
}Field Injection (Not Recommended):
@Service
public class UserService {
@Autowired // Avoid field injection
private UserRepository userRepository;
// Difficult to test and creates tight coupling
}Setter Injection (Optional Dependencies):
@Service
public class UserService {
private UserRepository userRepository;
private EmailService emailService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired(required = false)
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}Component Stereotypes:
@Component // Generic component
public class MyComponent { }
@Service // Business logic layer
public class MyService { }
@Repository // Data access layer
public class MyRepository { }
@Controller // Presentation layer (web)
public class MyController { }
@RestController // REST API controller
public class MyRestController { }Spring Web (REST APIs)
Build RESTful web services with Spring MVC annotations.
Basic REST Controller:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
User created = userService.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@RequestBody @Valid User user) {
return userService.update(id, user)
.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();
}
}Request Mapping Variations:
@RestController
@RequestMapping("/api/products")
public class ProductController {
// Query parameters
@GetMapping("/search")
public List<Product> search(@RequestParam String name,
@RequestParam(required = false) String category) {
return productService.search(name, category);
}
// Multiple path variables
@GetMapping("/categories/{categoryId}/products/{productId}")
public Product getProductInCategory(@PathVariable Long categoryId,
@PathVariable Long productId) {
return productService.findInCategory(categoryId, productId);
}
// Request headers
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id,
@RequestHeader("Accept-Language") String language) {
return productService.find(id, language);
}
// Matrix variables
@GetMapping("/{id}")
public Product getProductWithMatrix(@PathVariable Long id,
@MatrixVariable Map<String, String> filters) {
return productService.findWithFilters(id, filters);
}
}Response Handling:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// Return different status codes
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
Order created = orderService.create(order);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// Custom headers
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
Order order = orderService.findById(id);
return ResponseEntity.ok()
.header("X-Order-Version", order.getVersion().toString())
.body(order);
}
// No content response
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.delete(id);
return ResponseEntity.noContent().build();
}
}Spring Data JPA
Spring Data JPA provides repository abstractions for database access.
Entity Definition:
@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(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and setters
}Repository Interface:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query method - Spring Data generates implementation
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String name);
List<User> findByDepartmentId(Long departmentId);
// Custom JPQL query
@Query("SELECT u FROM User u WHERE u.email = ?1")
Optional<User> findByEmailQuery(String email);
// Named parameters
@Query("SELECT u FROM User u WHERE u.name LIKE %:name% AND u.department.id = :deptId")
List<User> searchByNameAndDepartment(@Param("name") String name,
@Param("deptId") Long deptId);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
Optional<User> findByEmailNative(String email);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateUserName(@Param("id") Long id, @Param("name") String name);
// Pagination and sorting
Page<User> findByDepartmentId(Long departmentId, Pageable pageable);
List<User> findByNameContaining(String name, Sort sort);
}Repository Usage:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
public User save(User user) {
return userRepository.save(user);
}
public List<User> findAll() {
return userRepository.findAll();
}
public Page<User> findAll(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
return userRepository.findAll(pageable);
}
public boolean delete(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
}Relationships:
// One-to-Many
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
}
// Many-to-Many
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}Configuration
Spring Boot uses application.properties or application.yml for configuration.
Application Properties:
# Server configuration
server.port=8080
server.servlet.context-path=/api
# Database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA configuration
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Logging
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.springframework.web=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Custom properties
app.name=My Application
app.version=1.0.0Application YAML:
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: user
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
logging:
level:
root: INFO
com.example: DEBUG
org.springframework.web: DEBUG
app:
name: My Application
version: 1.0.0Configuration Properties Class:
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name;
private String version;
private Security security = new Security();
public static class Security {
private int tokenExpiration = 3600;
private String secretKey;
// Getters and setters
}
// Getters and setters
}
// Usage
@Service
public class MyService {
private final AppConfig appConfig;
public MyService(AppConfig appConfig) {
this.appConfig = appConfig;
}
public void printConfig() {
System.out.println("App: " + appConfig.getName());
System.out.println("Version: " + appConfig.getVersion());
}
}Environment-Specific Configuration:
# application.properties (default)
spring.profiles.active=dev
# application-dev.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb_dev
logging.level.root=DEBUG
# application-prod.properties
spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb_prod
logging.level.root=WARNProfile-Specific Beans:
@Configuration
public class DatabaseConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return DataSourceBuilder.create().build();
}
}Spring Security
Implement authentication and authorization in your application.
Basic Security Configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}In-Memory Authentication:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}Database Authentication:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}JWT Authentication:
@Component
public class JwtTokenProvider {
@Value("${app.security.jwt.secret}")
private String jwtSecret;
@Value("${app.security.jwt.expiration}")
private int jwtExpiration;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException | MalformedJwtException | ExpiredJwtException |
UnsupportedJwtException | IllegalArgumentException ex) {
return false;
}
}
}
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService customUserDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
CustomUserDetailsService customUserDetailsService) {
this.tokenProvider = tokenProvider;
this.customUserDetailsService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}API Reference
Common Annotations
Core Spring Annotations:
@SpringBootApplication: Main application class@Component: Generic component@Service: Service layer component@Repository: Data access layer component@Configuration: Configuration class@Bean: Bean definition method@Autowired: Dependency injection@Value: Inject property values@Profile: Conditional beans based on profiles
Web Annotations:
@RestController: REST API controller@Controller: MVC controller@RequestMapping: Map HTTP requests@GetMapping: Map GET requests@PostMapping: Map POST requests@PutMapping: Map PUT requests@DeleteMapping: Map DELETE requests@PatchMapping: Map PATCH requests@PathVariable: Extract path variables@RequestParam: Extract query parameters@RequestBody: Extract request body@RequestHeader: Extract request headers@ResponseStatus: Set response status
Data Annotations:
@Entity: JPA entity@Table: Table mapping@Id: Primary key@GeneratedValue: Auto-generated values@Column: Column mapping@OneToOne: One-to-one relationship@OneToMany: One-to-many relationship@ManyToOne: Many-to-one relationship@ManyToMany: Many-to-many relationship@JoinColumn: Join column@JoinTable: Join table
Validation Annotations:
@Valid: Enable validation@NotNull: Field cannot be null@NotEmpty: Field cannot be empty@NotBlank: Field cannot be blank@Size: String or collection size@Min: Minimum value@Max: Maximum value@Email: Email format@Pattern: Regex pattern
Transaction Annotations:
@Transactional: Enable transaction management@Transactional(readOnly = true): Read-only transaction
Security Annotations:
@EnableWebSecurity: Enable security@PreAuthorize: Method-level authorization@PostAuthorize: Post-method authorization@Secured: Role-based access
Async and Scheduling:
@EnableAsync: Enable async processing@Async: Async method@EnableScheduling: Enable scheduling@Scheduled: Scheduled method
Workflow Patterns
REST API Design Pattern
Complete CRUD REST API:
// Entity
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Description is required")
private String description;
@NotNull(message = "Price is required")
@Min(value = 0, message = "Price must be positive")
private BigDecimal price;
@NotNull(message = "Stock is required")
@Min(value = 0, message = "Stock must be positive")
private Integer stock;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and setters
}
// Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String name);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
}
// Service
@Service
@Transactional
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional(readOnly = true)
public Page<Product> findAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public Optional<Product> findById(Long id) {
return productRepository.findById(id);
}
public Product create(Product product) {
return productRepository.save(product);
}
public Optional<Product> update(Long id, Product productDetails) {
return productRepository.findById(id)
.map(product -> {
product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
product.setStock(productDetails.getStock());
return productRepository.save(product);
});
}
public boolean delete(Long id) {
return productRepository.findById(id)
.map(product -> {
productRepository.delete(product);
return true;
})
.orElse(false);
}
}
// Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public Page<Product> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return productService.findAll(pageable);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.create(product);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable Long id,
@Valid @RequestBody Product product) {
return productService.update(id, product)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (productService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}Exception Handling Pattern
Global Exception Handler:
// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
// Error response
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
// Constructors, getters, setters
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequest(
BadRequestException ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Bad Request",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, Object> errors = new HashMap<>();
errors.put("timestamp", LocalDateTime.now());
errors.put("status", HttpStatus.BAD_REQUEST.value());
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fieldErrors.put(error.getField(), error.getDefaultMessage())
);
errors.put("errors", fieldErrors);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex,
WebRequest request) {
ErrorResponse error = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}Database Integration Pattern
Complete Database Setup:
// application.yml
/*
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: user
password: password
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
*/
// Flyway migrations (db/migration/V1__Create_users_table.sql)
/*
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_users_email ON users(email);
*/
// Entity with auditing
@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 name;
@Column(nullable = false)
private String password;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Getters and setters
}
// Enable JPA auditing
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}Testing Pattern
Unit Tests:
@SpringBootTest
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testFindById_Success() {
User user = new User();
user.setId(1L);
user.setEmail("test@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
Optional<User> result = userService.findById(1L);
assertTrue(result.isPresent());
assertEquals("test@example.com", result.get().getEmail());
verify(userRepository, times(1)).findById(1L);
}
@Test
void testFindById_NotFound() {
when(userRepository.findById(1L)).thenReturn(Optional.empty());
Optional<User> result = userService.findById(1L);
assertFalse(result.isPresent());
}
}Integration Tests:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Test
void testCreateUser_Success() throws Exception {
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("test@example.com"))
.andExpect(jsonPath("$.name").value("Test User"));
}
@Test
void testGetUser_Success() throws Exception {
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
User saved = userRepository.save(user);
mockMvc.perform(get("/api/users/" + saved.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(saved.getId()))
.andExpect(jsonPath("$.email").value("test@example.com"));
}
@Test
void testGetUser_NotFound() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}Best Practices
1. Use Constructor Injection
Constructor injection is the recommended approach for dependency injection.
// Good - Constructor injection
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
// Bad - Field injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}2. Use DTOs for API Requests/Responses
Don't expose entities directly through REST APIs.
// DTO
public class UserDTO {
private Long id;
private String email;
private String name;
// No password field exposed
// Getters and setters
}
// Mapper
@Component
public class UserMapper {
public UserDTO toDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setEmail(user.getEmail());
dto.setName(user.getName());
return dto;
}
public User toEntity(UserDTO dto) {
User user = new User();
user.setEmail(dto.getEmail());
user.setName(dto.getName());
return user;
}
}
// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(userMapper::toDTO)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}3. Use Validation
Always validate input data.
// Entity with validation
@Entity
public class User {
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
}
// Controller
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// Validation happens automatically
return ResponseEntity.ok(userService.save(user));
}4. Use Transactions Properly
Mark service methods with appropriate transaction settings.
@Service
@Transactional
public class OrderService {
@Transactional(readOnly = true)
public List<Order> findAll() {
return orderRepository.findAll();
}
@Transactional
public Order createOrder(Order order) {
// Multiple database operations in one transaction
Order saved = orderRepository.save(order);
inventoryService.decreaseStock(order.getItems());
emailService.sendOrderConfirmation(saved);
return saved;
}
}5. Use Pagination
Always paginate large datasets.
@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return productService.findAll(pageable);
}6. Handle Exceptions Globally
Use @RestControllerAdvice for centralized exception handling.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}7. Use Logging
Implement proper logging throughout your application.
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User createUser(User user) {
logger.info("Creating user with email: {}", user.getEmail());
try {
User saved = userRepository.save(user);
logger.info("User created successfully with id: {}", saved.getId());
return saved;
} catch (Exception e) {
logger.error("Error creating user: {}", e.getMessage(), e);
throw e;
}
}
}8. Secure Your Endpoints
Implement proper authentication and authorization.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt();
return http.build();
}
}9. Use Database Migrations
Use Flyway or Liquibase for database version control.
-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL
);
-- V2__Add_password_column.sql
ALTER TABLE users ADD COLUMN password VARCHAR(255);10. Monitor Your Application
Use Spring Boot Actuator for monitoring.
# application.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=alwaysExamples
See EXAMPLES.md for detailed code examples including:
- Basic Spring Boot Application
- REST API with CRUD Operations
- Database Integration with JPA
- Custom Queries and Specifications
- Request Validation
- Exception Handling
- Authentication with JWT
- Role-Based Authorization
- File Upload/Download
- Caching with Redis
- Async Processing
- Scheduled Tasks
- Multiple Database Configuration
- Actuator and Monitoring
- Docker Deployment
Summary
This Spring Boot development skill covers:
1. Auto-Configuration: Automatic configuration based on dependencies 2. Dependency Injection: IoC container, constructor injection, component stereotypes 3. REST APIs: Controllers, request mapping, response handling 4. Spring Data JPA: Entities, repositories, relationships, queries 5. Configuration: Properties, YAML, profiles, custom properties 6. Security: Authentication, authorization, JWT, role-based access 7. Exception Handling: Global exception handling, custom exceptions 8. Testing: Unit tests, integration tests, MockMvc 9. Best Practices: DTOs, validation, transactions, pagination, logging 10. Production Ready: Actuator, monitoring, database migrations, deployment
The patterns and examples are based on official Spring Boot documentation (Trust Score: 7.5) and represent modern enterprise Java development practices.
Spring Boot Development Examples
Comprehensive code examples demonstrating Spring Boot patterns, best practices, and real-world use cases.
Table of Contents
1. Basic Spring Boot Application 2. REST API with CRUD Operations 3. Database Integration with JPA 4. Custom Queries and Specifications 5. Request Validation 6. Exception Handling 7. Authentication with JWT 8. Role-Based Authorization 9. File Upload and Download 10. Caching with Redis 11. Async Processing 12. Scheduled Tasks 13. Email Service 14. Pagination and Sorting 15. Database Transactions 16. Actuator and Monitoring 17. Docker Deployment 18. API Versioning
---
1. Basic Spring Boot Application
Application Class:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}Simple Controller:
@RestController
@RequestMapping("/api")
public class WelcomeController {
@Value("${app.name}")
private String appName;
@GetMapping("/welcome")
public Map<String, Object> welcome() {
Map<String, Object> response = new HashMap<>();
response.put("message", "Welcome to " + appName);
response.put("timestamp", LocalDateTime.now());
response.put("status", "success");
return response;
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Application is running");
}
}Configuration:
# application.yml
app:
name: Spring Boot Demo Application
server:
port: 8080
logging:
level:
root: INFO
com.example.demo: DEBUG---
2. REST API with CRUD Operations
Entity:
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(length = 1000)
private String description;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private Integer stock;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors, getters, and setters
public Product() {}
public Product(String name, String description, BigDecimal price, Integer stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
// Getters and setters omitted for brevity
}Repository:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String name);
List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
List<Product> findByStockLessThan(Integer stock);
}Service:
@Service
@Transactional
public class ProductService {
private static final Logger logger = LoggerFactory.getLogger(ProductService.class);
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional(readOnly = true)
public List<Product> findAll() {
logger.debug("Fetching all products");
return productRepository.findAll();
}
@Transactional(readOnly = true)
public Optional<Product> findById(Long id) {
logger.debug("Fetching product with id: {}", id);
return productRepository.findById(id);
}
public Product create(Product product) {
logger.info("Creating new product: {}", product.getName());
return productRepository.save(product);
}
public Optional<Product> update(Long id, Product productDetails) {
logger.info("Updating product with id: {}", id);
return productRepository.findById(id)
.map(product -> {
product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
product.setStock(productDetails.getStock());
return productRepository.save(product);
});
}
public boolean delete(Long id) {
logger.info("Deleting product with id: {}", id);
return productRepository.findById(id)
.map(product -> {
productRepository.delete(product);
return true;
})
.orElse(false);
}
@Transactional(readOnly = true)
public List<Product> searchByName(String name) {
return productRepository.findByNameContaining(name);
}
@Transactional(readOnly = true)
public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
}Controller:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.findAll();
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product created = productService.create(product);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(
@PathVariable Long id,
@RequestBody Product product) {
return productService.update(id, product)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (productService.delete(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(@RequestParam String name) {
List<Product> products = productService.searchByName(name);
return ResponseEntity.ok(products);
}
@GetMapping("/price-range")
public ResponseEntity<List<Product>> getProductsByPriceRange(
@RequestParam BigDecimal min,
@RequestParam BigDecimal max) {
List<Product> products = productService.findByPriceRange(min, max);
return ResponseEntity.ok(products);
}
}---
3. Database Integration with JPA
Complex Entity with Relationships:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", nullable = false, unique = true)
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@Column(nullable = false)
private BigDecimal totalAmount;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (orderNumber == null) {
orderNumber = generateOrderNumber();
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Helper methods
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
calculateTotal();
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
calculateTotal();
}
private void calculateTotal() {
totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private String generateOrderNumber() {
return "ORD-" + System.currentTimeMillis();
}
// Getters and setters
}
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false)
private BigDecimal price;
@Column(nullable = false)
private BigDecimal subtotal;
@PrePersist
@PreUpdate
private void calculateSubtotal() {
if (price != null && quantity != null) {
subtotal = price.multiply(BigDecimal.valueOf(quantity));
}
}
// Getters and setters
}
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String phone;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
// Getters and setters
}
public enum OrderStatus {
PENDING,
CONFIRMED,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}Repository with Custom Queries:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
List<Order> findByCustomerId(@Param("customerId") Long customerId);
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatus(@Param("status") OrderStatus status);
@Query("SELECT o FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate")
List<Order> findByDateRange(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
}---
4. Custom Queries and Specifications
Specification Pattern:
public class ProductSpecification {
public static Specification<Product> hasName(String name) {
return (root, query, builder) ->
name == null ? null : builder.like(
builder.lower(root.get("name")),
"%" + name.toLowerCase() + "%"
);
}
public static Specification<Product> hasPriceGreaterThan(BigDecimal price) {
return (root, query, builder) ->
price == null ? null : builder.greaterThanOrEqualTo(root.get("price"), price);
}
public static Specification<Product> hasPriceLessThan(BigDecimal price) {
return (root, query, builder) ->
price == null ? null : builder.lessThanOrEqualTo(root.get("price"), price);
}
public static Specification<Product> hasStockGreaterThan(Integer stock) {
return (root, query, builder) ->
stock == null ? null : builder.greaterThan(root.get("stock"), stock);
}
}
@Repository
public interface ProductRepository extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
// Standard methods
}
@Service
public class ProductSearchService {
private final ProductRepository productRepository;
public ProductSearchService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> searchProducts(String name, BigDecimal minPrice,
BigDecimal maxPrice, Integer minStock) {
Specification<Product> spec = Specification.where(null);
if (name != null) {
spec = spec.and(ProductSpecification.hasName(name));
}
if (minPrice != null) {
spec = spec.and(ProductSpecification.hasPriceGreaterThan(minPrice));
}
if (maxPrice != null) {
spec = spec.and(ProductSpecification.hasPriceLessThan(maxPrice));
}
if (minStock != null) {
spec = spec.and(ProductSpecification.hasStockGreaterThan(minStock));
}
return productRepository.findAll(spec);
}
}Native Query Example:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = """
SELECT u.* FROM users u
LEFT JOIN orders o ON u.id = o.customer_id
WHERE o.created_at >= :startDate
GROUP BY u.id
HAVING COUNT(o.id) >= :minOrders
""", nativeQuery = true)
List<User> findActiveCustomers(
@Param("startDate") LocalDateTime startDate,
@Param("minOrders") int minOrders
);
@Modifying
@Query(value = "UPDATE users SET last_login = :loginTime WHERE id = :userId",
nativeQuery = true)
void updateLastLogin(@Param("userId") Long userId,
@Param("loginTime") LocalDateTime loginTime);
}---
5. Request Validation
Entity with Validation:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
@Column(unique = true)
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
message = "Password must contain at least one digit, one lowercase, one uppercase, and one special character"
)
private String password;
@NotNull(message = "Age is required")
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 120, message = "Age must be less than 120")
private Integer age;
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number")
private String phone;
// Getters and setters
}Custom Validator:
@Documented
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
private final UserRepository userRepository;
public UniqueEmailValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return true;
}
return !userRepository.existsByEmail(email);
}
}
// Usage
public class UserDTO {
@UniqueEmail
@Email
private String email;
}Validation Groups:
public interface CreateGroup {}
public interface UpdateGroup {}
@Entity
public class User {
@Null(groups = CreateGroup.class, message = "ID must be null when creating")
@NotNull(groups = UpdateGroup.class, message = "ID is required when updating")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
private String name;
@NotBlank(groups = CreateGroup.class)
@Null(groups = UpdateGroup.class, message = "Password cannot be updated")
private String password;
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(
@Validated(CreateGroup.class) @RequestBody User user) {
// Create logic
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Validated(UpdateGroup.class) @RequestBody User user) {
// Update logic
}
}---
6. Exception Handling
Custom Exceptions:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
}
}
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}Error Response DTO:
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
private Map<String, String> validationErrors;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters and setters
}Global Exception Handler:
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex,
WebRequest request) {
logger.error("Resource not found: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequest(
BadRequestException ex,
WebRequest request) {
logger.error("Bad request: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Bad Request",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(
MethodArgumentNotValidException ex,
WebRequest request) {
logger.error("Validation error: {}", ex.getMessage());
Map<String, String> validationErrors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
validationErrors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
"Input validation failed",
request.getDescription(false).replace("uri=", "")
);
error.setValidationErrors(validationErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrityViolation(
DataIntegrityViolationException ex,
WebRequest request) {
logger.error("Data integrity violation: {}", ex.getMessage());
String message = "Database constraint violation";
if (ex.getCause() instanceof ConstraintViolationException) {
message = "Duplicate entry or constraint violation";
}
ErrorResponse error = new ErrorResponse(
HttpStatus.CONFLICT.value(),
"Conflict",
message,
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex,
WebRequest request) {
logger.error("Unexpected error: ", ex);
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
"An unexpected error occurred",
request.getDescription(false).replace("uri=", "")
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}---
7. Authentication with JWT
JWT Utility Class:
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-ms}")
private long jwtExpirationMs;
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty");
}
return false;
}
}JWT Authentication Filter:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
UserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}Authentication Controller:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserService userService;
private final JwtTokenProvider tokenProvider;
private final PasswordEncoder passwordEncoder;
@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if (userService.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Email already in use"));
}
User user = new User();
user.setName(signUpRequest.getName());
user.setEmail(signUpRequest.getEmail());
user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
User result = userService.save(user);
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));
}
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(
(UserDetails) authentication.getPrincipal()
);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
}---
8. Role-Based Authorization
User with Roles:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Getters and setters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private RoleName name;
// Getters and setters
}
public enum RoleName {
ROLE_USER,
ROLE_ADMIN,
ROLE_MODERATOR
}UserDetails Implementation:
public class UserPrincipal implements UserDetails {
private Long id;
private String name;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String name, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getName(),
user.getEmail(),
user.getPassword(),
authorities
);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// Getters
}Security Configuration:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/moderator/**").hasAnyRole("ADMIN", "MODERATOR")
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}Method-Level Security:
@RestController
@RequestMapping("/api/users")
public class UserController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/me")
public User getCurrentUser(@CurrentUser UserPrincipal currentUser) {
return userService.findById(currentUser.getId());
}
@PreAuthorize("hasRole('ADMIN') or #id == principal.id")
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.ok().build();
}
}---
9. File Upload and Download
File Storage Service:
@Service
public class FileStorageService {
private final Path fileStorageLocation;
@Autowired
public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new RuntimeException("Could not create upload directory", ex);
}
}
public String storeFile(MultipartFile file) {
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
try {
if (fileName.contains("..")) {
throw new BadRequestException("Invalid file path: " + fileName);
}
String uniqueFileName = System.currentTimeMillis() + "_" + fileName;
Path targetLocation = this.fileStorageLocation.resolve(uniqueFileName);
Files.copy(file.getInputStream(), targetLocation,
StandardCopyOption.REPLACE_EXISTING);
return uniqueFileName;
} catch (IOException ex) {
throw new RuntimeException("Could not store file " + fileName, ex);
}
}
public Resource loadFileAsResource(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists()) {
return resource;
} else {
throw new ResourceNotFoundException("File", "name", fileName);
}
} catch (MalformedURLException ex) {
throw new ResourceNotFoundException("File", "name", fileName);
}
}
public void deleteFile(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Files.deleteIfExists(filePath);
} catch (IOException ex) {
throw new RuntimeException("Could not delete file " + fileName, ex);
}
}
}File Controller:
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileStorageService fileStorageService;
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@PostMapping("/upload")
public ResponseEntity<UploadFileResponse> uploadFile(
@RequestParam("file") MultipartFile file) {
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/api/files/download/")
.path(fileName)
.toUriString();
return ResponseEntity.ok(new UploadFileResponse(
fileName,
fileDownloadUri,
file.getContentType(),
file.getSize()
));
}
@PostMapping("/upload-multiple")
public ResponseEntity<List<UploadFileResponse>> uploadMultipleFiles(
@RequestParam("files") MultipartFile[] files) {
List<UploadFileResponse> responses = Arrays.stream(files)
.map(file -> {
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder
.fromCurrentContextPath()
.path("/api/files/download/")
.path(fileName)
.toUriString();
return new UploadFileResponse(
fileName,
fileDownloadUri,
file.getContentType(),
file.getSize()
);
})
.collect(Collectors.toList());
return ResponseEntity.ok(responses);
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(
@PathVariable String fileName,
HttpServletRequest request) {
Resource resource = fileStorageService.loadFileAsResource(fileName);
String contentType = null;
try {
contentType = request.getServletContext()
.getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException ex) {
logger.info("Could not determine file type.");
}
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
@DeleteMapping("/{fileName:.+}")
public ResponseEntity<?> deleteFile(@PathVariable String fileName) {
fileStorageService.deleteFile(fileName);
return ResponseEntity.ok(new ApiResponse(true, "File deleted successfully"));
}
}Configuration:
# application.yml
file:
upload-dir: ./uploads
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB---
10. Caching with Redis
Dependencies (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>Redis Configuration:
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}Service with Caching:
@Service
public class ProductService {
private final ProductRepository productRepository;
@Cacheable(value = "products", key = "#id")
public Optional<Product> findById(Long id) {
logger.info("Fetching product from database: {}", id);
return productRepository.findById(id);
}
@Cacheable(value = "products", unless = "#result.isEmpty()")
public List<Product> findAll() {
logger.info("Fetching all products from database");
return productRepository.findAll();
}
@CachePut(value = "products", key = "#product.id")
public Product save(Product product) {
logger.info("Saving product and updating cache: {}", product.getId());
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
logger.info("Deleting product and evicting from cache: {}", id);
productRepository.deleteById(id);
}
@CacheEvict(value = "products", allEntries = true)
public void clearCache() {
logger.info("Clearing all products from cache");
}
}Configuration:
spring:
redis:
host: localhost
port: 6379
password:
timeout: 2000ms
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0---
11. Async Processing
Enable Async:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}Async Service:
@Service
public class EmailService {
private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
@Async("taskExecutor")
public CompletableFuture<Void> sendEmail(String to, String subject, String body) {
logger.info("Sending email to: {}", to);
try {
// Simulate email sending
Thread.sleep(3000);
logger.info("Email sent successfully to: {}", to);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("Error sending email", e);
}
return CompletableFuture.completedFuture(null);
}
@Async
public CompletableFuture<String> processLongRunningTask(String data) {
logger.info("Starting long running task with data: {}", data);
try {
Thread.sleep(5000);
String result = "Processed: " + data;
logger.info("Task completed: {}", result);
return CompletableFuture.completedFuture(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("Task interrupted", e);
return CompletableFuture.failedFuture(e);
}
}
}Using Async Methods:
@Service
public class OrderService {
private final EmailService emailService;
private final NotificationService notificationService;
@Transactional
public Order createOrder(Order order) {
Order saved = orderRepository.save(order);
// These run asynchronously
emailService.sendEmail(
order.getCustomer().getEmail(),
"Order Confirmation",
"Your order has been confirmed"
);
notificationService.sendNotification(
order.getCustomer().getId(),
"Order placed successfully"
);
return saved;
}
public void processOrdersAsync(List<Long> orderIds) {
List<CompletableFuture<Void>> futures = orderIds.stream()
.map(id -> emailService.sendEmail("customer@example.com", "subject", "body"))
.collect(Collectors.toList());
// Wait for all to complete
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}---
12. Scheduled Tasks
Enable Scheduling:
@Configuration
@EnableScheduling
public class SchedulingConfig {
}Scheduled Service:
@Service
public class ScheduledTasks {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
private final OrderRepository orderRepository;
private final EmailService emailService;
// Execute at fixed rate (every 5 seconds)
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
logger.info("The time is now {}", LocalDateTime.now());
}
// Execute at fixed delay (5 seconds after previous execution completes)
@Scheduled(fixedDelay = 5000)
public void scheduleFixedDelayTask() {
logger.info("Fixed delay task - {}", LocalDateTime.now());
}
// Execute with initial delay
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void scheduleTaskWithInitialDelay() {
logger.info("Task with initial delay - {}", LocalDateTime.now());
}
// Execute using cron expression (every day at 2 AM)
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledDailyTask() {
logger.info("Daily task executed at 2 AM");
cleanupOldData();
}
// Every hour at minute 0
@Scheduled(cron = "0 0 * * * ?")
public void hourlyTask() {
logger.info("Hourly task - {}", LocalDateTime.now());
}
// Every Monday at 9 AM
@Scheduled(cron = "0 0 9 * * MON")
public void weeklyTask() {
logger.info("Weekly task - Monday 9 AM");
generateWeeklyReports();
}
// Business day task (Mon-Fri at 10 AM)
@Scheduled(cron = "0 0 10 * * MON-FRI")
public void businessDayTask() {
logger.info("Business day task");
}
private void cleanupOldData() {
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
orderRepository.deleteByCreatedAtBefore(cutoffDate);
logger.info("Deleted orders older than 30 days");
}
private void generateWeeklyReports() {
// Generate and send reports
logger.info("Generating weekly reports");
}
// Send daily summary email
@Scheduled(cron = "0 0 18 * * ?")
public void sendDailySummary() {
LocalDate today = LocalDate.now();
long orderCount = orderRepository.countByCreatedAtBetween(
today.atStartOfDay(),
today.plusDays(1).atStartOfDay()
);
emailService.sendEmail(
"admin@example.com",
"Daily Summary",
"Today's order count: " + orderCount
);
}
}---
13. Email Service
Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>Email Configuration:
spring:
mail:
host: smtp.gmail.com
port: 587
username: your-email@gmail.com
password: your-password
properties:
mail:
smtp:
auth: true
starttls:
enable: trueEmail Service:
@Service
public class EmailService {
private final JavaMailSender mailSender;
private final TemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String fromEmail;
// Send simple email
public void sendSimpleEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
}
// Send HTML email
public void sendHtmlEmail(String to, String subject, String htmlBody)
throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(fromEmail);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlBody, true);
mailSender.send(message);
}
// Send email with attachment
public void sendEmailWithAttachment(String to, String subject, String text,
String attachmentPath)
throws MessagingException, IOException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(fromEmail);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text);
FileSystemResource file = new FileSystemResource(new File(attachmentPath));
helper.addAttachment(file.getFilename(), file);
mailSender.send(message);
}
// Send email using Thymeleaf template
public void sendTemplatedEmail(String to, String subject,
String templateName,
Map<String, Object> variables)
throws MessagingException {
Context context = new Context();
context.setVariables(variables);
String htmlBody = templateEngine.process(templateName, context);
sendHtmlEmail(to, subject, htmlBody);
}
// Send order confirmation email
public void sendOrderConfirmation(Order order) throws MessagingException {
Map<String, Object> variables = new HashMap<>();
variables.put("customerName", order.getCustomer().getName());
variables.put("orderNumber", order.getOrderNumber());
variables.put("items", order.getItems());
variables.put("totalAmount", order.getTotalAmount());
sendTemplatedEmail(
order.getCustomer().getEmail(),
"Order Confirmation - " + order.getOrderNumber(),
"order-confirmation",
variables
);
}
}Thymeleaf Email Template (templates/order-confirmation.html):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Order Confirmation</title>
</head>
<body>
<h1>Order Confirmation</h1>
<p>Dear <span th:text="${customerName}">Customer</span>,</p>
<p>Thank you for your order! Your order number is: <strong th:text="${orderNumber}">123</strong></p>
<h2>Order Items:</h2>
<table>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
</tr>
<tr th:each="item : ${items}">
<td th:text="${item.product.name}">Product</td>
<td th:text="${item.quantity}">1</td>
<td th:text="${item.price}">$10.00</td>
</tr>
</table>
<p><strong>Total Amount: <span th:text="${totalAmount}">$100.00</span></strong></p>
</body>
</html>---
14. Pagination and Sorting
Service with Pagination:
@Service
public class ProductService {
private final ProductRepository productRepository;
@Transactional(readOnly = true)
public Page<Product> findAll(int page, int size, String sortBy, String direction) {
Sort.Direction sortDirection = direction.equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sortBy));
return productRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public Page<Product> searchProducts(String name, BigDecimal minPrice,
BigDecimal maxPrice, int page, int size) {
Specification<Product> spec = Specification.where(null);
if (name != null) {
spec = spec.and(ProductSpecification.hasName(name));
}
if (minPrice != null) {
spec = spec.and(ProductSpecification.hasPriceGreaterThan(minPrice));
}
if (maxPrice != null) {
spec = spec.and(ProductSpecification.hasPriceLessThan(maxPrice));
}
Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
return productRepository.findAll(spec, pageable);
}
}Controller with Pagination:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@GetMapping
public ResponseEntity<Page<Product>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
Page<Product> products = productService.findAll(page, size, sortBy, direction);
return ResponseEntity.ok(products);
}
@GetMapping("/search")
public ResponseEntity<Page<Product>> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<Product> products = productService.searchProducts(
name, minPrice, maxPrice, page, size
);
return ResponseEntity.ok(products);
}
}Custom Page Response:
public class PagedResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean last;
public PagedResponse(Page<T> page) {
this.content = page.getContent();
this.page = page.getNumber();
this.size = page.getSize();
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
this.last = page.isLast();
}
// Getters and setters
}
@GetMapping
public ResponseEntity<PagedResponse<Product>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<Product> productPage = productService.findAll(page, size, "id", "asc");
return ResponseEntity.ok(new PagedResponse<>(productPage));
}---
15. Database Transactions
Transaction Management:
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final EmailService emailService;
// Read-only transaction
@Transactional(readOnly = true)
public Optional<Order> findById(Long id) {
return orderRepository.findById(id);
}
// Default transaction (read-write)
public Order createOrder(Order order) {
// Validate stock
for (OrderItem item : order.getItems()) {
Product product = productRepository.findById(item.getProduct().getId())
.orElseThrow(() -> new ResourceNotFoundException(
"Product", "id", item.getProduct().getId()
));
if (product.getStock() < item.getQuantity()) {
throw new BadRequestException("Insufficient stock for product: " + product.getName());
}
// Decrease stock
product.setStock(product.getStock() - item.getQuantity());
productRepository.save(product);
}
// Save order
Order saved = orderRepository.save(order);
// Send email (if this fails, transaction rolls back)
try {
emailService.sendOrderConfirmation(saved);
} catch (Exception e) {
throw new RuntimeException("Failed to send order confirmation", e);
}
return saved;
}
// Custom transaction settings
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.SERIALIZABLE,
timeout = 30,
rollbackFor = Exception.class
)
public void processPayment(Long orderId, PaymentDetails payment) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", "id", orderId));
// Process payment
boolean paymentSuccess = paymentGateway.process(payment);
if (paymentSuccess) {
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
} else {
throw new RuntimeException("Payment failed");
}
}
// Programmatic transaction management
@Autowired
private TransactionTemplate transactionTemplate;
public Order createOrderProgrammatic(Order order) {
return transactionTemplate.execute(status -> {
try {
// Update stock
for (OrderItem item : order.getItems()) {
Product product = productRepository.findById(
item.getProduct().getId()
).orElseThrow();
product.setStock(product.getStock() - item.getQuantity());
productRepository.save(product);
}
// Save order
Order saved = orderRepository.save(order);
// Send email
emailService.sendOrderConfirmation(saved);
return saved;
} catch (Exception e) {
status.setRollbackOnly();
throw new RuntimeException("Order creation failed", e);
}
});
}
}---
16. Actuator and Monitoring
Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Configuration:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env,beans,mappings
base-path: /actuator
endpoint:
health:
show-details: always
show-components: always
metrics:
export:
prometheus:
enabled: true
info:
env:
enabled: true
info:
app:
name: Spring Boot Application
description: My Spring Boot Application
version: 1.0.0
encoding: @project.build.sourceEncoding@
java:
version: @java.version@Custom Health Indicator:
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Check some custom health condition
boolean isHealthy = checkCustomCondition();
if (isHealthy) {
return Health.up()
.withDetail("customService", "Available")
.withDetail("timestamp", LocalDateTime.now())
.build();
}
return Health.down()
.withDetail("customService", "Unavailable")
.withDetail("error", "Service is down")
.build();
}
private boolean checkCustomCondition() {
// Implement your health check logic
return true;
}
}Custom Metrics:
@Service
public class OrderService {
private final MeterRegistry meterRegistry;
private final Counter orderCounter;
private final Timer orderTimer;
public OrderService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.orderCounter = Counter.builder("orders.created")
.description("Total number of orders created")
.register(meterRegistry);
this.orderTimer = Timer.builder("orders.processing.time")
.description("Time taken to process orders")
.register(meterRegistry);
}
public Order createOrder(Order order) {
return orderTimer.record(() -> {
Order saved = orderRepository.save(order);
orderCounter.increment();
return saved;
});
}
}Available Endpoints:
GET /actuator/health- Application healthGET /actuator/info- Application infoGET /actuator/metrics- Application metricsGET /actuator/env- Environment propertiesGET /actuator/beans- Spring beansGET /actuator/mappings- Request mappings
---
17. Docker Deployment
Dockerfile:
# Build stage
FROM maven:3.8.5-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Run stage
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=password
depends_on:
- db
- redis
networks:
- app-network
db:
image: postgres:15
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridgeBuild and Run:
# Build image
docker build -t myapp:latest .
# Run with docker-compose
docker-compose up -d
# View logs
docker-compose logs -f app
# Stop
docker-compose down---
18. API Versioning
URL Versioning:
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1 getUser(@PathVariable Long id) {
return userService.findByIdV1(id);
}
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2 getUser(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}Header Versioning:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}", headers = "X-API-VERSION=1")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}", headers = "X-API-VERSION=2")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}Accept Header Versioning:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping(value = "/{id}",
produces = "application/vnd.myapp.v1+json")
public UserV1 getUserV1(@PathVariable Long id) {
return userService.findByIdV1(id);
}
@GetMapping(value = "/{id}",
produces = "application/vnd.myapp.v2+json")
public UserV2 getUserV2(@PathVariable Long id) {
return userService.findByIdV2(id);
}
}---
This examples file provides comprehensive, production-ready code examples for building Spring Boot applications. Each example demonstrates best practices and real-world patterns used in enterprise applications.
Spring Boot Development Skill
A comprehensive skill for building modern Spring Boot applications with REST APIs, database integration, security, and enterprise Java patterns.
Overview
Spring Boot is an opinionated framework built on top of the Spring Framework that makes it easy to create stand-alone, production-grade Spring-based applications. It provides auto-configuration, embedded servers, and production-ready features out of the box.
Key Features
- Auto-Configuration: Automatically configures Spring application based on dependencies
- Embedded Servers: Built-in Tomcat, Jetty, or Undertow - no need for WAR deployment
- Production-Ready: Actuator endpoints for monitoring, health checks, and metrics
- Starter Dependencies: Curated sets of dependencies for different use cases
- Convention over Configuration: Sensible defaults with minimal configuration
- Spring Ecosystem: Full access to Spring Framework, Spring Data, Spring Security, etc.
Getting Started
Prerequisites
- Java 17 or higher
- Maven 3.6+ or Gradle 7+
- IDE (IntelliJ IDEA, Eclipse, VS Code)
- Database (PostgreSQL, MySQL, H2, etc.)
Create a New Spring Boot Project
Using Spring Initializr (https://start.spring.io/):
1. Project: Maven or Gradle 2. Language: Java 3. Spring Boot: 3.x (latest stable) 4. Project Metadata:
- Group: com.example
- Artifact: myapp
- Package name: com.example.myapp
- Packaging: Jar
- Java: 17
5. Dependencies:
- Spring Web
- Spring Data JPA
- PostgreSQL Driver (or your database)
- Spring Security
- Validation
- Lombok (optional)
Using Maven:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>myapp</name>
<description>My Spring Boot Application</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Project Structure
myapp/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── myapp/
│ │ │ ├── MyAppApplication.java
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── AppConfig.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ ├── service/
│ │ │ │ └── UserService.java
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java
│ │ │ ├── model/
│ │ │ │ └── User.java
│ │ │ ├── dto/
│ │ │ │ └── UserDTO.java
│ │ │ └── exception/
│ │ │ ├── ResourceNotFoundException.java
│ │ │ └── GlobalExceptionHandler.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
└── README.mdBasic Application Setup
Main Application Class:
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}Configuration (application.yml):
spring:
application:
name: myapp
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
security:
user:
name: admin
password: admin
server:
port: 8080
servlet:
context-path: /api
logging:
level:
root: INFO
com.example.myapp: DEBUG
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: alwaysRunning the Application
Using Maven:
# Run the application
./mvnw spring-boot:run
# Run with specific profile
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Build and run jar
./mvnw clean package
java -jar target/myapp-0.0.1-SNAPSHOT.jarUsing Gradle:
# Run the application
./gradlew bootRun
# Build and run jar
./gradlew build
java -jar build/libs/myapp-0.0.1-SNAPSHOT.jarUsing IDE:
Run the main application class (MyAppApplication.java) directly from your IDE.
Quick Start Examples
1. Simple REST Controller
@RestController
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping
public String sayHello() {
return "Hello, Spring Boot!";
}
@GetMapping("/{name}")
public String sayHelloToName(@PathVariable String name) {
return "Hello, " + name + "!";
}
}Test: curl http://localhost:8080/api/hello/World
2. Entity and Repository
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and setters
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}3. Service Layer
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
public User save(User user) {
return userRepository.save(user);
}
}4. Complete CRUD Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User created = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@Valid @RequestBody User user) {
return userService.findById(id)
.map(existing -> {
user.setId(id);
return ResponseEntity.ok(userService.save(user));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.findById(id).isPresent()) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}Common Development Tasks
Database Setup
H2 In-Memory Database (for development):
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
path: /h2-consolePostgreSQL:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: passwordMySQL:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: passwordDatabase Migrations
Using Flyway:
Add dependency:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>Create migration: src/main/resources/db/migration/V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);Testing
Unit Test:
@SpringBootTest
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testFindById() {
User user = new User();
user.setId(1L);
user.setName("John");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
Optional<User> result = userService.findById(1L);
assertTrue(result.isPresent());
assertEquals("John", result.get().getName());
}
}Integration Test:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testGetUser() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1));
}
}Key Concepts to Master
1. Auto-Configuration: Understanding how Spring Boot automatically configures your application 2. Dependency Injection: Constructor injection, component scanning, bean lifecycle 3. REST APIs: Building RESTful services with proper HTTP methods and status codes 4. Data Access: JPA entities, repositories, relationships, queries 5. Security: Authentication, authorization, JWT, OAuth2 6. Exception Handling: Global exception handlers, custom exceptions 7. Validation: Bean validation, custom validators 8. Testing: Unit tests, integration tests, test slices 9. Configuration: Properties, profiles, externalized configuration 10. Production: Actuator, monitoring, logging, deployment
Resources
Next Steps
1. Review the SKILL.md file for comprehensive documentation 2. Check EXAMPLES.md for detailed code examples 3. Build a simple REST API project 4. Add database integration with Spring Data JPA 5. Implement authentication with Spring Security 6. Add validation and exception handling 7. Write unit and integration tests 8. Deploy your application
Common Annotations Quick Reference
| Annotation | Purpose |
|---|---|
@SpringBootApplication | Main application class |
@RestController | REST API controller |
@Service | Service layer component |
@Repository | Data access layer component |
@Entity | JPA entity |
@GetMapping | HTTP GET request handler |
@PostMapping | HTTP POST request handler |
@PutMapping | HTTP PUT request handler |
@DeleteMapping | HTTP DELETE request handler |
@PathVariable | Extract path variable |
@RequestParam | Extract query parameter |
@RequestBody | Extract request body |
@Valid | Enable validation |
@Transactional | Enable transaction management |
Troubleshooting
Application won't start
- Check if port 8080 is already in use
- Verify database connection settings
- Check for missing dependencies
- Review application logs
Database connection fails
- Verify database is running
- Check credentials in application.yml
- Ensure database driver dependency is included
- Check firewall settings
404 Not Found
- Verify controller path mapping
- Check if component scanning is configured correctly
- Ensure application context path is correct
Bean creation error
- Check for circular dependencies
- Verify all required dependencies are autowired
- Review bean scope and lifecycle
This README provides a solid foundation for getting started with Spring Boot development. Refer to SKILL.md for in-depth documentation and EXAMPLES.md for practical code examples.
Related skills
How it compares
Pick spring-boot-development for opinionated Spring Boot enterprise patterns; use framework-agnostic REST design skills when the JVM stack is not yet chosen.
FAQ
What Spring Boot topics does spring-boot-development cover?
spring-boot-development covers auto-configuration mechanics, constructor-based dependency injection, REST controller and ResponseEntity patterns, Spring Data JPA repositories, application profile configuration, and Spring Security setup for production Java services.
How do you install spring-boot-development?
Install spring-boot-development by running `npx skills add manutej/luxor-claude-marketplace --skill spring-boot-development`, which adds the skill from the LUXOR Claude Code Marketplace luxor-backend-toolkit plugin.
Does spring-boot-development prefer field or constructor injection?
spring-boot-development recommends constructor-based dependency injection over field injection for testability and immutability, documenting Spring bean wiring patterns aligned with current Spring Boot best practices.