Skip to content

Spring Boot

What is Spring Boot?

Spring Boot is an opinionated framework that simplifies Spring application development through: - Auto-configuration: Automatically configures beans based on classpath - Starter dependencies: Curated dependency sets for common use cases - Embedded servers: No external server deployment needed - Production-ready features: Metrics, health checks, externalized config

Spring Boot Architecture


Application Bootstrap

Main Class

@SpringBootApplication  // Combines 3 annotations
public class Application {

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

// @SpringBootApplication is equivalent to:
@Configuration           // This class provides beans
@EnableAutoConfiguration // Enable auto-configuration
@ComponentScan          // Scan this package and subpackages
public class Application { }

Customizing Startup

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(Application.class);

    app.setBannerMode(Banner.Mode.OFF);
    app.setWebApplicationType(WebApplicationType.SERVLET);
    app.setAdditionalProfiles("production");
    app.setDefaultProperties(Map.of(
        "server.port", "8080",
        "spring.application.name", "my-app"
    ));

    app.run(args);
}

// Fluent API
new SpringApplicationBuilder(Application.class)
    .bannerMode(Banner.Mode.OFF)
    .profiles("production")
    .run(args);

Application Events

@Component
public class StartupListener {

    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() {
        log.info("Application is ready to serve requests");
    }

    @EventListener(ContextRefreshedEvent.class)
    public void onContextRefreshed() {
        log.info("Context refreshed");
    }
}

// Event order:
// ApplicationStartingEvent
// ApplicationEnvironmentPreparedEvent
// ApplicationContextInitializedEvent
// ApplicationPreparedEvent
// ContextRefreshedEvent
// ApplicationStartedEvent
// ApplicationReadyEvent

CommandLineRunner & ApplicationRunner

@Component
public class DataLoader implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        // Runs after context is loaded
        loadInitialData();
    }
}

@Component
@Order(1)  // Run first
public class CacheWarmer implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Access to parsed command line args
        if (args.containsOption("warm-cache")) {
            warmCache();
        }
    }
}

Configuration

application.properties / application.yml

# application.yml
server:
  port: 8080
  servlet:
    context-path: /api

spring:
  application:
    name: my-service
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

logging:
  level:
    root: INFO
    com.example: DEBUG
    org.hibernate.SQL: DEBUG

# Custom properties
app:
  feature:
    new-checkout: true
  payment:
    timeout: 30s
    retry:
      max-attempts: 3
      delay: 1000

@ConfigurationProperties

@ConfigurationProperties(prefix = "app.payment")
@Validated
public class PaymentProperties {

    @NotNull
    private Duration timeout;

    @Valid
    private Retry retry = new Retry();

    // Getters and setters

    public static class Retry {
        private int maxAttempts = 3;
        private long delay = 1000;
        // Getters and setters
    }
}

// Enable scanning
@Configuration
@EnableConfigurationProperties(PaymentProperties.class)
public class AppConfig { }

// Or use component scan
@ConfigurationProperties(prefix = "app.payment")
@Component
public class PaymentProperties { }

// Usage
@Service
public class PaymentService {

    private final PaymentProperties properties;

    public PaymentService(PaymentProperties properties) {
        this.properties = properties;
    }

    public void process() {
        Duration timeout = properties.getTimeout();
        int maxRetries = properties.getRetry().getMaxAttempts();
    }
}

@Value Injection

@Service
public class NotificationService {

    @Value("${app.notification.enabled:true}")
    private boolean enabled;

    @Value("${app.notification.email.from}")
    private String fromEmail;

    @Value("${SMTP_PASSWORD}")  // Environment variable
    private String smtpPassword;

    @Value("#{${app.feature.flags}}")  // SpEL - Map
    private Map<String, Boolean> featureFlags;

    @Value("${app.allowed-origins}")  // Comma-separated to List
    private List<String> allowedOrigins;
}

Profiles

# application.yml (default)
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:development}

---
# application-development.yml
spring:
  config:
    activate:
      on-profile: development
  datasource:
    url: jdbc:h2:mem:testdb
  jpa:
    hibernate:
      ddl-auto: create-drop

---
# application-production.yml
spring:
  config:
    activate:
      on-profile: production
  datasource:
    url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
  jpa:
    hibernate:
      ddl-auto: validate

Starters

Common Starters

Spring Boot Starters

Dependency Management

// build.gradle
plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Version managed by Spring Boot BOM
    implementation 'com.fasterxml.jackson.core:jackson-databind'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Auto-Configuration

How It Works

Auto-Configuration Process

Conditional Annotations

@Configuration
@ConditionalOnClass(DataSource.class)          // Class on classpath
@ConditionalOnMissingBean(DataSource.class)    // No bean defined
@ConditionalOnProperty(
    prefix = "spring.datasource",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true
)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder.create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

Excluding Auto-Configuration

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    SecurityAutoConfiguration.class
})
public class Application { }

// Or in properties
spring.autoconfigure.exclude=\
  org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Debug Auto-Configuration

# See what auto-configs were applied
java -jar app.jar --debug

# Or
logging.level.org.springframework.boot.autoconfigure=DEBUG

Web Development

REST Controllers

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public List<OrderDTO> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return orderService.findAll(PageRequest.of(page, size));
    }

    @GetMapping("/{id}")
    public ResponseEntity<OrderDTO> get(@PathVariable Long id) {
        return orderService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderDTO create(@Valid @RequestBody CreateOrderRequest request) {
        return orderService.create(request);
    }

    @PutMapping("/{id}")
    public OrderDTO update(@PathVariable Long id,
                          @Valid @RequestBody UpdateOrderRequest request) {
        return orderService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        orderService.delete(id);
    }
}

Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return new ErrorResponse("VALIDATION_ERROR", errors);
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        log.error("Unhandled exception", ex);
        return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred");
    }
}

public record ErrorResponse(String code, Object message) {}

Request Validation

public record CreateOrderRequest(
    @NotNull @Size(min = 1)
    List<@Valid OrderItem> items,

    @NotBlank
    String customerId,

    @Valid
    ShippingAddress shippingAddress
) {}

public record OrderItem(
    @NotBlank String productId,
    @Min(1) int quantity,
    @Positive BigDecimal price
) {}

public record ShippingAddress(
    @NotBlank String street,
    @NotBlank String city,
    @Pattern(regexp = "\\d{5}") String zipCode
) {}

Actuator

Setup

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
  endpoint:
    health:
      show-details: when_authorized
  health:
    diskspace:
      enabled: true
    db:
      enabled: true
  info:
    env:
      enabled: true

Common Endpoints

Actuator Endpoints

Custom Health Indicator

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final RestTemplate restTemplate;

    @Override
    public Health health() {
        try {
            ResponseEntity<String> response = restTemplate.getForEntity(
                "https://api.external.com/health", String.class);

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("status", "External API is reachable")
                    .build();
            }
            return Health.down()
                .withDetail("status", "External API returned: " + response.getStatusCode())
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

Custom Metrics

@Service
public class OrderService {

    private final Counter orderCounter;
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry meterRegistry) {
        this.orderCounter = meterRegistry.counter("orders.created");
        this.orderProcessingTimer = meterRegistry.timer("orders.processing.time");
    }

    public Order create(CreateOrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(request);
            orderCounter.increment();
            return order;
        });
    }
}

Testing

Unit Tests

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void shouldCreateOrder() {
        // Given
        CreateOrderRequest request = new CreateOrderRequest(...);
        when(paymentService.charge(any())).thenReturn(PaymentResult.success());
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // When
        Order result = orderService.create(request);

        // Then
        assertThat(result.getStatus()).isEqualTo(OrderStatus.CREATED);
        verify(orderRepository).save(any(Order.class));
    }
}

Integration Tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldCreateOrder() {
        CreateOrderRequest request = new CreateOrderRequest(...);

        ResponseEntity<OrderDTO> response = restTemplate.postForEntity(
            "/api/v1/orders", request, OrderDTO.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().id()).isNotNull();

        assertThat(orderRepository.findById(response.getBody().id())).isPresent();
    }
}

Slice Tests

// Web layer only
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void shouldReturnOrder() throws Exception {
        when(orderService.findById(1L))
            .thenReturn(Optional.of(new OrderDTO(1L, "CREATED")));

        mockMvc.perform(get("/api/v1/orders/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.status").value("CREATED"));
    }
}

// Repository layer only
@DataJpaTest
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByCustomerId() {
        Order order = new Order();
        order.setCustomerId("cust-123");
        entityManager.persist(order);

        List<Order> orders = orderRepository.findByCustomerId("cust-123");

        assertThat(orders).hasSize(1);
    }
}

Test Configuration

@TestConfiguration
public class TestConfig {

    @Bean
    @Primary
    public PaymentService mockPaymentService() {
        PaymentService mock = mock(PaymentService.class);
        when(mock.charge(any())).thenReturn(PaymentResult.success());
        return mock;
    }
}

// Use Testcontainers for real dependencies
@SpringBootTest
@Testcontainers
class OrderServiceIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @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);
    }
}

Common Interview Questions

  1. What is @SpringBootApplication?
  2. Combines @Configuration, @EnableAutoConfiguration, @ComponentScan

  3. How does auto-configuration work?

  4. Scans META-INF for auto-config classes
  5. Uses @Conditional annotations to decide what to configure
  6. Your beans take precedence

  7. Externalized configuration priority?

  8. Command line args > Profile-specific > application.properties > @PropertySource

  9. How to create custom starter?

  10. Create auto-configuration class with @Conditional
  11. Add META-INF/spring.factories
  12. Create starter module with dependencies

  13. Embedded server vs external?

  14. Embedded: Simpler deployment, app owns server
  15. External: Traditional deployment, shared server

  • *