Spring Boot

The de facto standard framework for Java enterprise application development. Enables rapid development through auto-configuration and rich ecosystem.

JavaFrameworkBackendWebSpringEnterpriseMicroservices

GitHub Overview

spring-projects/spring-boot

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.

Stars78,081
Watchers3,343
Forks41,398
Created:October 19, 2012
Language:Java
License:Apache License 2.0

Topics

frameworkjavaspringspring-boot

Star History

spring-projects/spring-boot Star History
Data as of: 8/13/2025, 01:43 AM

Framework

Spring Boot

Overview

Spring Boot is the most popular web application framework in the Java ecosystem, designed for rapid and easy development of production-grade Spring applications.

Details

Spring Boot is a framework for creating standalone, production-grade Spring-based applications quickly and easily, built on top of the Spring Framework. Since its release in 2014, it has established itself as the de facto standard for Java development. The auto-configuration feature significantly reduces the complex XML configuration required in traditional Spring, allowing web applications to be built with minimal code. With embedded Tomcat and Jetty servers, it creates executable applications as single JAR files, simplifying deployment. Dependency Injection (DI) and Aspect-Oriented Programming (AOP) enable the development of highly maintainable and extensible applications. Currently widely used in microservices architecture and cloud-native application development, it's utilized for enterprise system construction in combination with a rich ecosystem including Spring Cloud, Spring Security, and other related technologies. Spring Boot 3.x now supports Java 17+, GraalVM native images, and improved observability features.

Pros and Cons

Pros

  • Auto-configuration: Automates complex configurations, dramatically improving development efficiency
  • Embedded Servers: Built-in Tomcat and Jetty for easy web application execution
  • Starter Dependencies: Packages necessary features together, simplifying dependency management
  • Production-Ready Features: Actuator provides monitoring and management capabilities for operations
  • Rich Ecosystem: Diverse related technologies like Spring Cloud and Spring Security
  • Microservices Ready: Lightweight and fast startup, ideal for microservices
  • Enterprise Support: Long-term support and continuous development by VMware Tanzu
  • High Market Value: High demand in enterprise systems with strong career prospects

Cons

  • Learning Curve: Understanding the entire Spring ecosystem requires significant time investment
  • Memory Usage: Framework overhead tends to consume more memory
  • Startup Time: Longer startup times due to DI container initialization and AOP processing
  • Feature Overkill: May have excessive functionality for simple applications
  • Black Box Effect: Auto-configuration can make internal operations less transparent
  • Java Dependency: Tied to Java language and JVM, cannot be used with other languages

Key Links

Code Examples

Hello World (Basic Web Application)

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

    @GetMapping("/")
    public String hello() {
        return "Hello, Spring Boot World!";
    }

    @GetMapping("/api/status")
    public String status() {
        return "Application is running!";
    }

    @GetMapping("/greet/{name}")
    public String greet(@PathVariable String name) {
        return "Hello, " + name + "!";
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

// Project creation commands:
// Visit https://start.spring.io/ or use Spring CLI:
// spring init --dependencies=web --name=demo demo
// cd demo
// ./mvnw spring-boot:run

REST API Development

package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo.service.UserService;
import com.example.demo.model.User;
import java.util.List;

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*") // Allow CORS for frontend integration
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.findAll();
        return ResponseEntity.ok(users);
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.findById(id)
                .map(user -> ResponseEntity.ok(user))
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody User user) {
        try {
            User savedUser = userService.save(user);
            return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
        } catch (Exception e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        return userService.findById(id)
                .map(user -> {
                    user.setName(userDetails.getName());
                    user.setEmail(userDetails.getEmail());
                    return ResponseEntity.ok(userService.save(user));
                })
                .orElse(ResponseEntity.notFound().build());
    }

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

    @GetMapping("/search")
    public ResponseEntity<List<User>> searchUsers(@RequestParam String name) {
        List<User> users = userService.findByNameContaining(name);
        return ResponseEntity.ok(users);
    }
}

// User model class
package com.example.demo.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Name is required")
    @Column(nullable = false)
    private String name;
    
    @Email(message = "Email should be valid")
    @NotBlank(message = "Email is required")
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    // Constructors
    public User() {}

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

Database Operations (Spring Data JPA)

package com.example.demo.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.DecimalMin;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Product name is required")
    @Column(nullable = false)
    private String name;
    
    @Column(columnDefinition = "TEXT")
    private String description;
    
    @DecimalMin(value = "0.0", message = "Price must be positive")
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Review> reviews;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    // Constructors, getters, and setters
    public Product() {}

    public Product(String name, String description, BigDecimal price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    // Getters and setters omitted for brevity
}

// Repository interface with custom queries
package com.example.demo.repository;

import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

public interface ProductRepository extends JpaRepository<Product, Long> {
    
    // Query methods using method naming
    List<Product> findByNameContainingIgnoreCase(String name);
    
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
    
    Optional<Product> findByNameIgnoreCase(String name);
    
    // Custom JPQL queries
    @Query("SELECT p FROM Product p WHERE p.price > :price")
    List<Product> findExpensiveProducts(@Param("price") BigDecimal price);
    
    @Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
           "OR LOWER(p.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
    Page<Product> searchProducts(@Param("keyword") String keyword, Pageable pageable);
    
    // Native SQL query
    @Query(value = "SELECT * FROM products p WHERE p.price >= ?1 ORDER BY p.created_at DESC LIMIT ?2", 
           nativeQuery = true)
    List<Product> findTopExpensiveProducts(BigDecimal minPrice, int limit);
}

// Service layer with business logic
package com.example.demo.service;

import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

@Service
@Transactional
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public List<Product> findAll() {
        return productRepository.findAll();
    }

    public Page<Product> findAllPaginated(int page, int size, String sortBy) {
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by(sortBy));
        return productRepository.findAll(pageRequest);
    }

    public Optional<Product> findById(Long id) {
        return productRepository.findById(id);
    }

    @Transactional
    public Product save(Product product) {
        return productRepository.save(product);
    }

    @Transactional
    public void deleteById(Long id) {
        productRepository.deleteById(id);
    }

    public boolean existsById(Long id) {
        return productRepository.existsById(id);
    }

    public List<Product> searchByName(String name) {
        return productRepository.findByNameContainingIgnoreCase(name);
    }

    public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByPriceBetween(minPrice, maxPrice);
    }

    public Page<Product> searchProducts(String keyword, int page, int size) {
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return productRepository.searchProducts(keyword, pageRequest);
    }
}

Dependency Injection and Bean Management

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@Configuration
public class AppConfig {

    @Bean
    @Primary
    public NotificationService emailNotificationService() {
        return new EmailNotificationService();
    }

    @Bean
    @Profile("production")
    public NotificationService smsNotificationService() {
        return new SmsNotificationService();
    }

    @Bean
    @ConditionalOnProperty(name = "app.slack.enabled", havingValue = "true")
    public NotificationService slackNotificationService() {
        return new SlackNotificationService();
    }
}

// Service interfaces and implementations
package com.example.demo.service;

public interface NotificationService {
    void sendNotification(String message, String recipient);
    boolean isAvailable();
}

// Email implementation
package com.example.demo.service.impl;

import com.example.demo.service.NotificationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class EmailNotificationService implements NotificationService {
    
    private static final Logger logger = LoggerFactory.getLogger(EmailNotificationService.class);
    
    @Value("${app.email.enabled:true}")
    private boolean emailEnabled;
    
    @Override
    public void sendNotification(String message, String recipient) {
        if (emailEnabled) {
            logger.info("Sending email to {}: {}", recipient, message);
            // Email sending logic here
        } else {
            logger.warn("Email service is disabled");
        }
    }
    
    @Override
    public boolean isAvailable() {
        return emailEnabled;
    }
}

// SMS implementation
@Service
public class SmsNotificationService implements NotificationService {
    
    private static final Logger logger = LoggerFactory.getLogger(SmsNotificationService.class);
    
    @Override
    public void sendNotification(String message, String recipient) {
        logger.info("Sending SMS to {}: {}", recipient, message);
        // SMS sending logic here
    }
    
    @Override
    public boolean isAvailable() {
        return true; // Assume SMS is always available
    }
}

// Usage in controller with dependency injection
package com.example.demo.controller;

import com.example.demo.service.NotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.List;

@RestController
@RequestMapping("/api/notifications")
public class NotificationController {

    private final NotificationService primaryNotificationService;
    private final List<NotificationService> allNotificationServices;

    // Constructor injection (recommended approach)
    public NotificationController(@Qualifier("emailNotificationService") NotificationService primaryService,
                                  List<NotificationService> allServices) {
        this.primaryNotificationService = primaryService;
        this.allNotificationServices = allServices;
    }

    @PostMapping("/send")
    public ResponseEntity<String> sendNotification(@RequestParam String message, 
                                                   @RequestParam String recipient) {
        primaryNotificationService.sendNotification(message, recipient);
        return ResponseEntity.ok("Notification sent via primary service");
    }

    @PostMapping("/broadcast")
    public ResponseEntity<String> broadcastNotification(@RequestParam String message, 
                                                        @RequestParam String recipient) {
        allNotificationServices.forEach(service -> {
            if (service.isAvailable()) {
                service.sendNotification(message, recipient);
            }
        });
        return ResponseEntity.ok("Notification broadcasted to all available services");
    }

    @GetMapping("/services")
    public ResponseEntity<List<String>> getAvailableServices() {
        List<String> availableServices = allNotificationServices.stream()
                .filter(NotificationService::isAvailable)
                .map(service -> service.getClass().getSimpleName())
                .toList();
        return ResponseEntity.ok(availableServices);
    }
}

Configuration Properties and Validation

package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationProperties {
    
    @NotBlank(message = "Application name is required")
    private String name;
    
    @NotBlank(message = "Version is required")
    @Pattern(regexp = "\\d+\\.\\d+\\.\\d+", message = "Version must follow semantic versioning")
    private String version;
    
    private Database database = new Database();
    private Security security = new Security();
    private Cache cache = new Cache();
    
    // Feature flags
    private Map<String, Boolean> features;
    
    // Getters and Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public String getVersion() { return version; }
    public void setVersion(String version) { this.version = version; }
    
    public Database getDatabase() { return database; }
    public void setDatabase(Database database) { this.database = database; }
    
    public Security getSecurity() { return security; }
    public void setSecurity(Security security) { this.security = security; }
    
    public Cache getCache() { return cache; }
    public void setCache(Cache cache) { this.cache = cache; }
    
    public Map<String, Boolean> getFeatures() { return features; }
    public void setFeatures(Map<String, Boolean> features) { this.features = features; }
    
    public static class Database {
        @NotBlank(message = "Database host is required")
        private String host = "localhost";
        
        @Min(value = 1, message = "Port must be greater than 0")
        @Max(value = 65535, message = "Port must be less than 65536")
        private int port = 5432;
        
        @NotBlank(message = "Database name is required")
        private String name;
        
        @Min(value = 1, message = "Pool size must be at least 1")
        private int maxPoolSize = 20;
        
        private Duration connectionTimeout = Duration.ofSeconds(30);
        
        // Getters and setters
        public String getHost() { return host; }
        public void setHost(String host) { this.host = host; }
        
        public int getPort() { return port; }
        public void setPort(int port) { this.port = port; }
        
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        
        public int getMaxPoolSize() { return maxPoolSize; }
        public void setMaxPoolSize(int maxPoolSize) { this.maxPoolSize = maxPoolSize; }
        
        public Duration getConnectionTimeout() { return connectionTimeout; }
        public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; }
    }
    
    public static class Security {
        private boolean enabled = true;
        
        @NotEmpty(message = "At least one allowed origin is required")
        private List<String> allowedOrigins = List.of("http://localhost:3000");
        
        private Jwt jwt = new Jwt();
        
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
        
        public List<String> getAllowedOrigins() { return allowedOrigins; }
        public void setAllowedOrigins(List<String> allowedOrigins) { this.allowedOrigins = allowedOrigins; }
        
        public Jwt getJwt() { return jwt; }
        public void setJwt(Jwt jwt) { this.jwt = jwt; }
        
        public static class Jwt {
            @NotBlank(message = "JWT secret key is required")
            private String secretKey;
            
            private Duration expiration = Duration.ofHours(24);
            
            public String getSecretKey() { return secretKey; }
            public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
            
            public Duration getExpiration() { return expiration; }
            public void setExpiration(Duration expiration) { this.expiration = expiration; }
        }
    }
    
    public static class Cache {
        private boolean enabled = true;
        private String type = "memory";
        private Duration ttl = Duration.ofMinutes(30);
        
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
        
        public String getType() { return type; }
        public void setType(String type) { this.type = type; }
        
        public Duration getTtl() { return ttl; }
        public void setTtl(Duration ttl) { this.ttl = ttl; }
    }
}

// application.yml configuration example
/*
app:
  name: My Spring Boot Application
  version: 1.0.0
  database:
    host: localhost
    port: 5432
    name: myapp_db
    max-pool-size: 20
    connection-timeout: PT30S
  security:
    enabled: true
    allowed-origins:
      - http://localhost:3000
      - https://myapp.com
    jwt:
      secret-key: ${JWT_SECRET_KEY:fallback-secret-key}
      expiration: PT24H
  cache:
    enabled: true
    type: redis
    ttl: PT30M
  features:
    new-dashboard: true
    beta-api: false
    analytics: true
*/

// Using configuration properties in a component
package com.example.demo.component;

import com.example.demo.config.ApplicationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class AppInfoComponent {

    private static final Logger logger = LoggerFactory.getLogger(AppInfoComponent.class);
    
    private final ApplicationProperties appProperties;

    public AppInfoComponent(ApplicationProperties appProperties) {
        this.appProperties = appProperties;
    }

    @PostConstruct
    public void logApplicationInfo() {
        logger.info("Starting application: {}", appProperties.getName());
        logger.info("Version: {}", appProperties.getVersion());
        logger.info("Database: {}:{}/{}", 
                   appProperties.getDatabase().getHost(),
                   appProperties.getDatabase().getPort(),
                   appProperties.getDatabase().getName());
        logger.info("Security enabled: {}", appProperties.getSecurity().isEnabled());
        logger.info("Cache enabled: {}", appProperties.getCache().isEnabled());
        
        if (appProperties.getFeatures() != null) {
            appProperties.getFeatures().forEach((feature, enabled) -> {
                logger.info("Feature '{}': {}", feature, enabled ? "ENABLED" : "DISABLED");
            });
        }
    }
    
    public boolean isFeatureEnabled(String featureName) {
        return appProperties.getFeatures() != null && 
               Boolean.TRUE.equals(appProperties.getFeatures().get(featureName));
    }
}

Testing (JUnit 5, Mockito, and TestContainers)

package com.example.demo.controller;

import com.example.demo.service.ProductService;
import com.example.demo.entity.Product;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Optional;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@WebMvcTest(ProductController.class)
@DisplayName("Product Controller Tests")
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProductService productService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    @DisplayName("Should return all products")
    void testGetAllProducts() throws Exception {
        // Given
        Product product1 = new Product("Laptop", "Gaming laptop", new BigDecimal("999.99"));
        Product product2 = new Product("Mouse", "Wireless mouse", new BigDecimal("29.99"));
        when(productService.findAll()).thenReturn(Arrays.asList(product1, product2));

        // When & Then
        mockMvc.perform(get("/api/products"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].name").value("Laptop"))
                .andExpected(jsonPath("$[0].price").value(999.99))
                .andExpect(jsonPath("$[1].name").value("Mouse"));

        verify(productService).findAll();
    }

    @Test
    @DisplayName("Should return product by ID")
    void testGetProductById() throws Exception {
        // Given
        Product product = new Product("Keyboard", "Mechanical keyboard", new BigDecimal("129.99"));
        product.setId(1L);
        when(productService.findById(1L)).thenReturn(Optional.of(product));

        // When & Then
        mockMvc.perform(get("/api/products/1"))
                .andExpect(status().isOk())
                .andExpected(jsonPath("$.id").value(1))
                .andExpected(jsonPath("$.name").value("Keyboard"))
                .andExpected(jsonPath("$.price").value(129.99));

        verify(productService).findById(1L);
    }

    @Test
    @DisplayName("Should return 404 when product not found")
    void testGetProductByIdNotFound() throws Exception {
        // Given
        when(productService.findById(999L)).thenReturn(Optional.empty());

        // When & Then
        mockMvc.perform(get("/api/products/999"))
                .andExpect(status().isNotFound());

        verify(productService).findById(999L);
    }

    @Test
    @DisplayName("Should create new product")
    void testCreateProduct() throws Exception {
        // Given
        Product newProduct = new Product("Monitor", "4K Monitor", new BigDecimal("399.99"));
        Product savedProduct = new Product("Monitor", "4K Monitor", new BigDecimal("399.99"));
        savedProduct.setId(1L);
        
        when(productService.save(any(Product.class))).thenReturn(savedProduct);

        // When & Then
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(newProduct)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Monitor"))
                .andExpected(jsonPath("$.price").value(399.99));

        verify(productService).save(any(Product.class));
    }

    @Test
    @DisplayName("Should update existing product")
    void testUpdateProduct() throws Exception {
        // Given
        Product existingProduct = new Product("Old Name", "Old Description", new BigDecimal("100.00"));
        existingProduct.setId(1L);
        
        Product updateRequest = new Product("New Name", "New Description", new BigDecimal("150.00"));
        Product updatedProduct = new Product("New Name", "New Description", new BigDecimal("150.00"));
        updatedProduct.setId(1L);
        
        when(productService.findById(1L)).thenReturn(Optional.of(existingProduct));
        when(productService.save(any(Product.class))).thenReturn(updatedProduct);

        // When & Then
        mockMvc.perform(put("/api/products/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("New Name"))
                .andExpected(jsonPath("$.price").value(150.00));

        verify(productService).findById(1L);
        verify(productService).save(any(Product.class));
    }
}

// Integration test with TestContainers
package com.example.demo;

import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@DisplayName("Product Integration Tests")
class ProductIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ProductRepository productRepository;

    @Test
    @DisplayName("Should create and retrieve product end-to-end")
    void testCreateAndRetrieveProduct() {
        // Given
        Product product = new Product("Integration Test Product", "Test Description", new BigDecimal("99.99"));

        // When - Create product
        ResponseEntity<Product> createResponse = restTemplate.postForEntity(
            "http://localhost:" + port + "/api/products", 
            product, 
            Product.class
        );

        // Then - Verify creation
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        assertThat(createResponse.getBody().getName()).isEqualTo("Integration Test Product");
        assertThat(createResponse.getBody().getId()).isNotNull();

        // When - Retrieve product
        Long productId = createResponse.getBody().getId();
        ResponseEntity<Product> getResponse = restTemplate.getForEntity(
            "http://localhost:" + port + "/api/products/" + productId,
            Product.class
        );

        // Then - Verify retrieval
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody()).isNotNull();
        assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test Product");
        assertThat(getResponse.getBody().getPrice()).isEqualByComparingTo(new BigDecimal("99.99"));

        // Verify database state
        assertThat(productRepository.findById(productId)).isPresent();
    }

    @Test
    @DisplayName("Should handle product search functionality")
    void testProductSearch() {
        // Given - Setup test data
        productRepository.save(new Product("Gaming Laptop", "High-end gaming laptop", new BigDecimal("1299.99")));
        productRepository.save(new Product("Office Laptop", "Business laptop", new BigDecimal("799.99")));
        productRepository.save(new Product("Gaming Mouse", "RGB gaming mouse", new BigDecimal("49.99")));

        // When - Search for gaming products
        ResponseEntity<Product[]> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/api/products/search?name=gaming",
            Product[].class
        );

        // Then - Verify search results
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody()).hasSize(2);
        assertThat(response.getBody()[0].getName()).containsIgnoringCase("gaming");
        assertThat(response.getBody()[1].getName()).containsIgnoringCase("gaming");
    }
}

// Service layer unit test
package com.example.demo.service;

import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("Product Service Tests")
class ProductServiceTest {

    @Mock
    private ProductRepository productRepository;

    @InjectMocks
    private ProductService productService;

    @Test
    @DisplayName("Should save product successfully")
    void testSaveProduct() {
        // Given
        Product product = new Product("Test Product", "Test Description", new BigDecimal("199.99"));
        Product savedProduct = new Product("Test Product", "Test Description", new BigDecimal("199.99"));
        savedProduct.setId(1L);
        
        when(productRepository.save(any(Product.class))).thenReturn(savedProduct);

        // When
        Product result = productService.save(product);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getName()).isEqualTo("Test Product");
        verify(productRepository).save(product);
    }

    @Test
    @DisplayName("Should find product by ID")
    void testFindById() {
        // Given
        Product product = new Product("Test Product", "Description", new BigDecimal("99.99"));
        product.setId(1L);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        // When
        Optional<Product> result = productService.findById(1L);

        // Then
        assertThat(result).isPresent();
        assertThat(result.get().getName()).isEqualTo("Test Product");
        verify(productRepository).findById(1L);
    }

    @Test
    @DisplayName("Should return empty when product not found")
    void testFindByIdNotFound() {
        // Given
        when(productRepository.findById(999L)).thenReturn(Optional.empty());

        // When
        Optional<Product> result = productService.findById(999L);

        // Then
        assertThat(result).isEmpty();
        verify(productRepository).findById(999L);
    }
}

Spring Boot Actuator and Monitoring

// Custom health indicator
package com.example.demo.health;

import org.springframework.boot.actuator.health.Health;
import org.springframework.boot.actuator.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Health health() {
        try {
            // Test database connectivity
            jdbcTemplate.queryForObject("SELECT 1", Integer.class);
            return Health.up()
                    .withDetail("database", "PostgreSQL")
                    .withDetail("status", "Connected")
                    .build();
        } catch (Exception e) {
            return Health.down()
                    .withDetail("database", "PostgreSQL")
                    .withDetail("error", e.getMessage())
                    .build();
        }
    }
}

// Custom metrics
package com.example.demo.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Component
public class ProductMetrics {

    private final Counter productCreatedCounter;
    private final Timer productSearchTimer;
    private final MeterRegistry meterRegistry;

    public ProductMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.productCreatedCounter = Counter.builder("products.created")
                .description("Number of products created")
                .register(meterRegistry);
        this.productSearchTimer = Timer.builder("products.search.duration")
                .description("Product search duration")
                .register(meterRegistry);
    }

    public void incrementProductCreated() {
        productCreatedCounter.increment();
    }

    public Timer.Sample startSearchTimer() {
        return Timer.start(meterRegistry);
    }

    public void recordSearchTime(Timer.Sample sample) {
        sample.stop(productSearchTimer);
    }

    public void recordSearchTime(long duration, TimeUnit unit) {
        productSearchTimer.record(duration, unit);
    }
}

// application.yml for Actuator configuration
/*
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,env,beans
      base-path: /actuator
  endpoint:
    health:
      show-details: always
      show-components: always
  metrics:
    export:
      prometheus:
        enabled: true
  info:
    env:
      enabled: true

info:
  app:
    name: ${app.name}
    version: ${app.version}
    description: Spring Boot Demo Application
  build:
    artifact: ${project.artifactId}
    version: ${project.version}
*/