Spring Framework

Spring Boot
Complete Reference

A comprehensive guide to Spring Boot — from IoC container fundamentals through the presentation, business, and data layers to security, scheduling, and observability.

Spring Boot 3.x Java 17+ Spring MVC 6 Spring Data JPA Spring Security 6
01 — Introduction

What is Spring Boot?

Spring Boot is an opinionated, convention-over-configuration framework that sits on top of the Spring Framework. It eliminates the XML boilerplate and manual wiring that traditional Spring required, replacing it with auto-configuration, embedded servers, and starter dependencies — letting you build production-ready applications with minimal setup.

The three core promises: auto-configure everything sensible, embed Tomcat/Jetty directly, and provide production-ready features out of the box (health checks, metrics, externalized configuration).

Convention over configuration: If you add spring-boot-starter-data-jpa to your classpath, Spring Boot automatically configures a DataSource, EntityManagerFactory, and transaction manager — no XML, no @Bean definitions required unless you want to override the defaults.
@SpringBootApplication

Single entry point

One annotation bootstraps your entire application context.

spring-boot-starters

Curated dependencies

Add one starter; get a complete, tested dependency set.

Embedded server

Fat JAR deployment

Ship a single executable JAR — no Tomcat installation needed.

Actuator

Production ready

Health, metrics, and info endpoints available by default.

02 — Architecture

Architecture overview

A Spring Boot application is organized into three horizontal tiers — Presentation, Business Logic, and Data — all wired together by the IoC (Inversion of Control) container, known as the ApplicationContext. The container manages beans, resolves dependencies through injection, and controls the lifecycle of every component.

IoC Container — ApplicationContext Manages beans · dependency injection · lifecycle · events @SpringBootApplication · Auto-Configuration · Embedded Tomcat / Jetty · application.properties Presentation Layer @RestController maps HTTP endpoints @RequestMapping URL routing @ControllerAdvice global error handling FilterChain security / logging Business Logic Layer @Service business logic @Transactional tx management @Component generic bean @Async / @Scheduled background tasks Data Layer @Repository JPA / CRUD @Entity maps to table JPA / Hibernate ORM engine DataSource connection pool Database
03 — Bootstrap

Auto-Configuration & @SpringBootApplication

The entry point of any Spring Boot app is a single class annotated with @SpringBootApplication. Under the hood this is three annotations in one: @Configuration, @EnableAutoConfiguration, and @ComponentScan.

@SpringBootApplication   // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

// What happens on run():
// 1. Creates ApplicationContext
// 2. Scans @Component, @Service, @Repository, @Controller in the package tree
// 3. Auto-configures beans based on classpath (DataSource, JPA, Tomcat, ...)
// 4. Runs ApplicationRunner / CommandLineRunner beans
// 5. Starts the embedded web server
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.0</version>
</parent>

<dependencies>
  <!-- Web MVC + embedded Tomcat -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- JPA + Hibernate -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>

  <!-- Spring Security -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <!-- Bean 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>
</dependencies>
// Override auto-configured beans with your own @Bean definitions
@Configuration
public class AppConfig {

    // Override default ObjectMapper (Jackson JSON serializer)
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .registerModule(new JavaTimeModule());
    }

    // Declare a shared RestTemplate bean
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
            .setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(10))
            .build();
    }

    // ConditionalOnMissingBean — only creates this if you haven't defined your own
    @Bean
    @ConditionalOnMissingBean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}
04 — Presentation Layer

Controller layer

Controllers are the entry points for HTTP requests. Annotated with @RestController (which combines @Controller and @ResponseBody), they map URL paths to Java methods and handle serialization automatically via Jackson.

Core annotations

AnnotationPurposeHTTP method
@GetMappingRead a resource or listGET
@PostMappingCreate a new resourcePOST
@PutMappingFull replacement of a resourcePUT
@PatchMappingPartial update of a resourcePATCH
@DeleteMappingDelete a resourceDELETE
@RequestBodyDeserialize request JSON into a Java object
@PathVariableExtract value from URL segment /users/{id}
@RequestParamExtract query parameter ?page=0&size=10
@ResponseStatusSet the default HTTP status of a method
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserService userService;

    // Constructor injection — preferred over @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

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

    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto createUser(@Valid @RequestBody CreateUserRequest req) {
        return userService.create(req);
    }

    @PutMapping("/{id}")
    public UserDto updateUser(@PathVariable Long id,
                              @Valid @RequestBody UpdateUserRequest req) {
        return userService.update(id, req);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}
// ResponseEntity gives you fine-grained control over status, headers, body

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping("/{id}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) {
        return userService.findOptional(id)
            .map(ResponseEntity::ok)                           // 200 OK
            .orElse(ResponseEntity.notFound().build());        // 404
    }

    @PostMapping
    public ResponseEntity<ProductDto> createProduct(@RequestBody CreateProductRequest req) {
        ProductDto created = productService.create(req);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.id())
            .toUri();
        return ResponseEntity
            .created(location)            // 201 + Location header
            .body(created);
    }

    @GetMapping("/export")
    public ResponseEntity<byte[]> exportCsv() {
        byte[] csv = reportService.generateCsv();
        return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename=products.csv")
            .contentType(MediaType.TEXT_PLAIN)
            .body(csv);
    }
}
// DTOs (Data Transfer Objects) decouple API contracts from domain entities

// Immutable request DTO using Java 17 record
public record CreateUserRequest(
    @NotBlank  @Size(min=2, max=50)  String name,
    @NotBlank  @Email                String email,
    @NotBlank  @Size(min=8)          String password
) {}

// Response DTO — never expose the raw @Entity
public record UserDto(
    Long   id,
    String name,
    String email,
    LocalDateTime createdAt
) {
    // Static factory method from entity
    public static UserDto from(User user) {
        return new UserDto(user.getId(), user.getName(),
                           user.getEmail(), user.getCreatedAt());
    }
}

// Update DTO — all fields optional for PATCH
public record UpdateUserRequest(
    @Size(min=2, max=50) String name,   // null = unchanged
    @Email               String email
) {}
05 — Business Logic Layer

Service layer

The service layer holds all business logic, orchestrates calls to repositories, and enforces transactional boundaries. It is injected into controllers via the IoC container and should never be called directly by the data layer.

The golden rule: controllers call services; services call repositories. A controller should never call a repository directly, and a repository should never contain business logic.
@Service   // marks as a Spring-managed component; also communicates intent
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final ApplicationEventPublisher eventPublisher;

    // Constructor injection — all dependencies are final (immutable)
    public UserService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       ApplicationEventPublisher eventPublisher) {
        this.userRepository  = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.eventPublisher  = eventPublisher;
    }

    public List<UserDto> findAll(Pageable pageable) {
        return userRepository.findAll(pageable)
            .stream()
            .map(UserDto::from)
            .toList();
    }

    public UserDto findById(Long id) {
        return userRepository.findById(id)
            .map(UserDto::from)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    }

    @Transactional
    public UserDto create(CreateUserRequest req) {
        if (userRepository.existsByEmail(req.email())) {
            throw new EmailAlreadyExistsException(req.email());
        }
        User user = new User();
        user.setName(req.name());
        user.setEmail(req.email());
        user.setPassword(passwordEncoder.encode(req.password()));
        User saved = userRepository.save(user);
        eventPublisher.publishEvent(new UserCreatedEvent(saved));
        return UserDto.from(saved);
    }

    @Transactional
    public void delete(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
        userRepository.delete(user);
    }
}
// @Transactional wraps the entire method in a single database transaction.
// If an unchecked exception escapes, the transaction is automatically rolled back.

@Service
public class OrderService {

    @Transactional                         // default: propagation=REQUIRED, rollbackOn=RuntimeException
    public Order placeOrder(PlaceOrderRequest req) {
        // All of these run in ONE transaction:
        inventoryService.reserve(req.items());     // deduct stock
        Order order = orderRepository.save(...);   // persist order
        paymentService.charge(req.payment());      // record payment
        // If ANY step throws RuntimeException → entire transaction rolls back
        return order;
    }

    @Transactional(readOnly = true)        // hint to JPA: skip dirty checking → faster
    public List<Order> getOrdersForUser(Long userId) {
        return orderRepository.findByUserId(userId);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)  // always a fresh transaction
    public void auditLog(String action) {
        auditRepository.save(new AuditEntry(action, LocalDateTime.now()));
    }

    @Transactional(rollbackFor = CheckedPaymentException.class)  // also rollback on checked
    public void processPayment(Payment p) throws CheckedPaymentException {
        // ...
    }
}
06 — Data Access Layer

Repository layer

Spring Data JPA automatically implements repositories — you declare an interface extending JpaRepository<Entity, ID> and Spring generates all the SQL at startup. No EntityManager code required for standard operations.

// JpaRepository provides: save, findById, findAll, delete, count, existsById...
@Repository   // optional — Spring Data detects Repository interfaces automatically
public interface UserRepository extends JpaRepository<User, Long> {

    // Derived query — Spring generates SQL from the method name
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
    List<User> findByActiveTrue();
    List<User> findByCreatedAtAfter(LocalDateTime since);

    // Sorting and paging built-in
    Page<User> findByActive(boolean active, Pageable pageable);

    // Derived delete
    void deleteByEmail(String email);
}
public interface UserRepository extends JpaRepository<User, Long> {

    // JPQL query (entity-based, database-agnostic)
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
    Optional<User> findActiveByEmail(@Param("email") String email);

    // Native SQL query (database-specific)
    @Query(value = "SELECT * FROM users WHERE LOWER(name) LIKE LOWER(:pattern)",
           nativeQuery = true)
    List<User> searchByName(@Param("pattern") String pattern);

    // Projection — return only selected fields
    @Query("SELECT u.id AS id, u.name AS name, u.email AS email FROM User u")
    List<UserSummary> findAllSummaries();

    // Modifying query — must pair with @Transactional
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :cutoff")
    int deactivateInactiveUsers(@Param("cutoff") LocalDateTime cutoff);
}

// Projection interface
interface UserSummary {
    Long   getId();
    String getName();
    String getEmail();
}
// Specifications enable dynamic queries built at runtime
public interface UserRepository extends JpaRepository<User, Long>,
                                         JpaSpecificationExecutor<User> {}

// Specification builder
public class UserSpecs {
    public static Specification<User> hasEmail(String email) {
        return (root, query, cb) -> cb.equal(root.get("email"), email);
    }
    public static Specification<User> isActive() {
        return (root, query, cb) -> cb.isTrue(root.get("active"));
    }
    public static Specification<User> createdAfter(LocalDateTime date) {
        return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
    }
}

// Usage — compose specs dynamically
@Service
public class UserService {
    public List<User> search(String email, boolean onlyActive) {
        Specification<User> spec = Specification.where(null);
        if (email != null)   spec = spec.and(UserSpecs.hasEmail(email));
        if (onlyActive)      spec = spec.and(UserSpecs.isActive());
        return userRepository.findAll(spec);
    }
}
07 — Domain Model

Entity & Data Model

JPA entities are plain Java classes annotated with @Entity. Hibernate (the default JPA provider in Spring Boot) maps them to database tables, manages the object lifecycle, and generates SQL automatically.

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "full_name", nullable = false, length = 100)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private boolean active = true;

    @CreationTimestamp   // Hibernate sets on INSERT
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp     // Hibernate sets on UPDATE
    private LocalDateTime updatedAt;

    // Getters and setters ...
}
@Entity @Table(name = "orders")
public class Order {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Many orders belong to one user
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    // One order has many line items — cascade: persist/delete propagates
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @Enumerated(EnumType.STRING)
    private OrderStatus status = OrderStatus.PENDING;

    // Helper method to maintain bidirectional consistency
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

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

    @Column(nullable = false)
    private String productName;
    private int quantity;
    private BigDecimal unitPrice;
}
// Enable JPA Auditing once in your @Configuration
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class MyApplication { ... }

@Bean
public AuditorAware<String> auditorProvider() {
    return () -> Optional.ofNullable(SecurityContextHolder.getContext())
        .map(ctx -> ctx.getAuthentication())
        .map(auth -> auth.getName());
}

// Now annotate your entity to auto-capture audit fields
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Product {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate   @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy   @Column(updatable = false, length = 100)
    private String createdBy;

    @LastModifiedBy  @Column(length = 100)
    private String updatedBy;
}
08 — Request Flow

Request lifecycle

Every HTTP request travels through a strict pipeline before reaching your controller. Understanding this pipeline is essential for debugging, adding cross-cutting behaviour, and knowing where each concern lives.

Client HTTP req Security Filter Chain Dispatcher Servlet Controller @RestController Service @Service Repository @Repository Database SQL Response flows back → JSON serialized → HTTP 200
09 — Error Handling

Exception handling

Spring Boot provides two complementary mechanisms. A local @ExceptionHandler inside a controller handles exceptions from that controller only. A @RestControllerAdvice class provides a global fallback for every controller in the application. Local handlers take priority over global ones.

@ExceptionHandler

Local — one controller

  • Method inside @RestController
  • Highest priority
  • For controller-specific behavior
@RestControllerAdvice

Global — all controllers

  • Separate class, application-wide
  • Fallback after local handlers
  • Centralises the error JSON format
public record ErrorResponse(
    int           status,
    String        message,
    String        path,
    LocalDateTime timestamp
) {
    public static ErrorResponse of(int status, String message, String path) {
        return new ErrorResponse(status, message, path, LocalDateTime.now());
    }
}
// Result JSON: { "status":404, "message":"User not found: 42",
//               "path":"/api/users/42", "timestamp":"2024-03-15T10:30:00" }
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex,
                                        HttpServletRequest req) {
        return ErrorResponse.of(404, ex.getMessage(), req.getRequestURI());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex,
                                          HttpServletRequest req) {
        String msg = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .collect(Collectors.joining(", "));
        return ErrorResponse.of(400, msg, req.getRequestURI());
    }

    @ExceptionHandler(DataIntegrityViolationException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorResponse handleConflict(DataIntegrityViolationException ex,
                                        HttpServletRequest req) {
        return ErrorResponse.of(409, "Data conflict: " + ex.getRootCause().getMessage(),
                                req.getRequestURI());
    }

    // Always add this last — prevents raw stack traces reaching the client
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneric(Exception ex, HttpServletRequest req) {
        log.error("Unhandled exception at {}", req.getRequestURI(), ex);
        return ErrorResponse.of(500, "An unexpected error occurred",
                                req.getRequestURI());
    }
}
// Typed exception — carries resource info for clear messages
public class ResourceNotFoundException extends RuntimeException {
    private final String resourceType;
    private final Object resourceId;

    public ResourceNotFoundException(String type, Object id) {
        super(type + " not found with id: " + id);
        this.resourceType = type;
        this.resourceId   = id;
    }
    // getters ...
}

// Throw from service layer — caught by GlobalExceptionHandler
public User findById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", id));
}
10 — Input Validation

Validation with Bean Validation

Add spring-boot-starter-validation to your classpath and Spring Boot wires Jakarta Bean Validation automatically. Annotate request DTO fields with constraint annotations, then add @Valid to the controller parameter.

public record RegisterRequest(

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 80, message = "Name must be 2–80 characters")
    String name,

    @NotBlank @Email(message = "Must be a valid email address")
    String email,

    @NotBlank
    @Pattern(regexp = "^(?=.*[A-Z])(?=.*\\d).{8,}$",
             message = "Password needs 8+ chars, one uppercase and one digit")
    String password,

    @NotNull @Min(18) @Max(120)
    Integer age,

    @NotNull @Future(message = "Appointment must be in the future")
    LocalDate appointmentDate
) {}

// Controller: add @Valid to trigger validation
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public UserDto register(@Valid @RequestBody RegisterRequest req) {
    return userService.register(req);
    // Fails → MethodArgumentNotValidException → GlobalExceptionHandler → 400
}
// 1. Define the annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "Email is already registered";
    Class<?>[] groups()  default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. Implement the ConstraintValidator
@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 ctx) {
        return email != null && !userRepository.existsByEmail(email);
    }
}

// 3. Use it on a DTO field
public record RegisterRequest(
    @UniqueEmail
    @Email
    String email,
    // ...
) {}
11 — Security

Spring Security

Spring Security plugs in as a filter chain that runs before the DispatcherServlet. Every request passes through SecurityFilterChain — if authentication fails, your controller is never reached.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)          // disable for stateless APIs
            .sessionManagement(s ->
                s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()           // public
                .requestMatchers("/api/admin/**").hasRole("ADMIN")     // admin only
                .requestMatchers(HttpMethod.GET, "/api/products").permitAll()
                .anyRequest().authenticated()                          // everything else
            )
            .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

    @Bean
    public AuthenticationManager authManager(AuthenticationConfiguration cfg)
            throws Exception {
        return cfg.getAuthenticationManager();
    }
}
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        final String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        String token   = header.substring(7);
        String subject = jwtService.extractSubject(token);

        if (subject != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails user = userDetailsService.loadUserByUsername(subject);
            if (jwtService.isTokenValid(token, user)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        user, null, user.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource()
                    .buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}
// Enable method-level security in @Configuration
@EnableMethodSecurity
public class SecurityConfig { ... }

// Now protect individual service methods
@Service
public class DocumentService {

    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public Document getDocument(Long userId, Long docId) {
        return documentRepository.findByUserIdAndId(userId, docId)
            .orElseThrow(() -> new ResourceNotFoundException("Document", docId));
    }

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteDocument(Long docId) {
        documentRepository.deleteById(docId);
    }

    @PostAuthorize("returnObject.owner == authentication.name")
    public Document findSecure(Long docId) {
        return documentRepository.findById(docId)
            .orElseThrow();
    }
}
12 — Configuration

Externalized configuration

Spring Boot reads configuration from application.properties or application.yml. You can inject individual values with @Value or bind whole prefixes to a typed class using @ConfigurationProperties.

spring:
  application:
    name: my-service

  datasource:
    url:      jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}           # injected from environment variable
    password: ${DB_PASS}
    hikari:
      maximum-pool-size: 10

  jpa:
    hibernate:
      ddl-auto: validate           # never use create/create-drop in production
    show-sql: false
    properties:
      hibernate.format_sql: true

  security:
    jwt:
      secret:      ${JWT_SECRET}
      expiry-ms:   3600000         # 1 hour

server:
  port: 8080
  error:
    include-message: always        # include error messages in responses

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
// Bind the "security.jwt.*" prefix to a typed record
@ConfigurationProperties(prefix = "security.jwt")
public record JwtProperties(
    String secret,
    long   expiryMs
) {}

// Register it in your main class or a @Configuration class
@SpringBootApplication
@ConfigurationPropertiesScan   // auto-detects all @ConfigurationProperties
public class MyApplication { ... }

// Inject and use it
@Service
public class JwtService {
    private final JwtProperties jwtProps;

    public JwtService(JwtProperties jwtProps) {
        this.jwtProps = jwtProps;
    }

    public String generateToken(UserDetails user) {
        return Jwts.builder()
            .subject(user.getUsername())
            .expiration(new Date(System.currentTimeMillis() + jwtProps.expiryMs()))
            .signWith(Keys.hmacShaKeyFor(jwtProps.secret().getBytes()))
            .compact();
    }
}
# application.yml — default (shared) config
spring:
  jpa:
    show-sql: false

---
# application-dev.yml — activated with: --spring.profiles.active=dev
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:testdb    # in-memory DB for local dev
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: ${DB_URL}             # real database in production
  jpa:
    hibernate:
      ddl-auto: validate
13 — Background Tasks

Scheduling & Async

Spring Boot supports declarative scheduled tasks and asynchronous method execution. Enable them once with @EnableScheduling and @EnableAsync, then annotate individual methods.

@SpringBootApplication
@EnableScheduling
public class MyApplication { ... }

@Service
public class ScheduledTasks {

    // Fixed delay — waits 5 s after each completion before starting the next
    @Scheduled(fixedDelay = 5_000)
    public void pollExternalService() {
        externalApi.fetchUpdates().forEach(this::processUpdate);
    }

    // Fixed rate — fires every 10 s regardless of how long execution takes
    @Scheduled(fixedRate = 10_000)
    public void heartbeat() {
        log.info("Service alive at {}", LocalDateTime.now());
    }

    // Cron — every day at 02:00 UTC
    @Scheduled(cron = "0 0 2 * * *", zone = "UTC")
    public void nightly() {
        reportService.generate();
        cleanupService.purgeOldRecords();
    }

    // Dynamic cron from config
    @Scheduled(cron = "${tasks.sync.cron:0 */15 * * * *}")
    public void syncFromPartner() {
        partnerService.sync();
    }
}
@SpringBootApplication
@EnableAsync
public class MyApplication { ... }

// Custom thread pool (optional — recommended for production)
@Configuration
public class AsyncConfig {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(4);
        exec.setMaxPoolSize(16);
        exec.setQueueCapacity(100);
        exec.setThreadNamePrefix("async-");
        exec.initialize();
        return exec;
    }
}

@Service
public class NotificationService {

    // Returns immediately; Spring runs this on a separate thread
    @Async("taskExecutor")
    public CompletableFuture<Void> sendEmailAsync(String to, String body) {
        emailClient.send(to, body);           // may take a few seconds
        return CompletableFuture.completedFuture(null);
    }

    @Async
    public void sendPushNotificationAsync(Long userId, String message) {
        pushGateway.send(userId, message);
    }
}

// Controller — fire-and-forget, responds instantly
@PostMapping("/notify/{userId}")
@ResponseStatus(HttpStatus.ACCEPTED)
public void notify(@PathVariable Long userId, @RequestBody String message) {
    notificationService.sendPushNotificationAsync(userId, message);
    // returns 202 immediately while notification sends in background
}
14 — Observability

Spring Boot Actuator

Actuator exposes production-ready HTTP endpoints that give you live insight into your running application — database connectivity, JVM memory, HTTP metrics, bean wiring, and more. Add the starter and configure which endpoints to expose.

EndpointDescriptionDefault exposure
/actuator/healthApplication + dependency health (DB, disk, cache)exposed
/actuator/infoApp version, git commit, custom infoexposed
/actuator/metricsJVM, HTTP request latency, custom countersdisabled
/actuator/envAll resolved config properties and their sourcessensitive
/actuator/beansEvery bean registered in the ApplicationContextdisabled
/actuator/loggersView and change log levels at runtimedisabled
/actuator/httptraceLast 100 HTTP requests/responsesdisabled
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,loggers   # comma-separated or "*" for all
  endpoint:
    health:
      show-details: when-authorized            # or "always" / "never"
  info:
    env:
      enabled: true

# Expose build info in /actuator/info
info:
  app:
    name:    ${spring.application.name}
    version: "@project.version@"              # injected from pom.xml at build
// Custom health indicator — appears under /actuator/health
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final ExternalApiClient apiClient;

    @Override
    public Health health() {
        try {
            boolean ok = apiClient.ping();
            return ok
                ? Health.up().withDetail("api", "reachable").build()
                : Health.down().withDetail("api", "unreachable").build();
        } catch (Exception ex) {
            return Health.down()
                .withDetail("api", "error")
                .withException(ex)
                .build();
        }
    }
}

// Custom metric counter
@Service
public class OrderService {
    private final Counter orderCounter;

    public OrderService(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Total orders placed")
            .register(registry);
    }

    @Transactional
    public Order placeOrder(PlaceOrderRequest req) {
        Order order = // ... create order
        orderCounter.increment();
        return order;
    }
}
15 — Testing

Testing

Spring Boot ships first-class testing support via spring-boot-starter-test, which bundles JUnit 5, Mockito, AssertJ, and MockMvc. Tests fall into three categories by scope.

// Pure unit test — no Spring context loaded, very fast
class UserServiceTest {

    @Mock UserRepository userRepository;
    @Mock PasswordEncoder passwordEncoder;
    @InjectMocks UserService userService;

    @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }

    @Test
    void shouldReturnUserWhenFound() {
        User user = new User(1L, "Alice", "alice@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        UserDto result = userService.findById(1L);

        assertThat(result.name()).isEqualTo("Alice");
        verify(userRepository, times(1)).findById(1L);
    }

    @Test
    void shouldThrowWhenUserNotFound() {
        when(userRepository.findById(99L)).thenReturn(Optional.empty());
        assertThatThrownBy(() -> userService.findById(99L))
            .isInstanceOf(ResourceNotFoundException.class)
            .hasMessageContaining("User not found with id: 99");
    }
}
// @WebMvcTest loads ONLY the web layer (no JPA, no full context)
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired MockMvc mvc;
    @MockBean  UserService userService;   // replaced with Mockito mock

    @Test
    void getUser_returns200() throws Exception {
        UserDto dto = new UserDto(1L, "Alice", "alice@example.com", LocalDateTime.now());
        when(userService.findById(1L)).thenReturn(dto);

        mvc.perform(get("/api/v1/users/1").accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("Alice"))
           .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void createUser_returns400_whenEmailInvalid() throws Exception {
        String body = """{"name":"Bob","email":"not-an-email","password":"Secure1!"}""";
        mvc.perform(post("/api/v1/users")
               .contentType(MediaType.APPLICATION_JSON)
               .content(body))
           .andExpect(status().isBadRequest());
    }
}
// Full Spring context + in-memory H2 database
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = Replace.ANY)   // swap to H2
class UserIntegrationTest {

    @Autowired TestRestTemplate restTemplate;
    @Autowired UserRepository   userRepository;

    @BeforeEach void setup() { userRepository.deleteAll(); }

    @Test
    void createAndFetchUser() {
        CreateUserRequest req = new CreateUserRequest("Alice","alice@test.com","Secret1!");
        ResponseEntity<UserDto> createResp =
            restTemplate.postForEntity("/api/v1/users", req, UserDto.class);

        assertThat(createResp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        Long id = createResp.getBody().id();

        ResponseEntity<UserDto> getResp =
            restTemplate.getForEntity("/api/v1/users/" + id, UserDto.class);

        assertThat(getResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResp.getBody().email()).isEqualTo("alice@test.com");
    }
}
16 — Quick Reference

Annotations cheatsheet

Stereotype & component

AnnotationLayerPurpose
@SpringBootApplicationBootstrapCombines @Configuration + @EnableAutoConfiguration + @ComponentScan
@ComponentAnyGeneric Spring-managed bean; discovered by component scan
@ServiceBusinessMarks a service bean; communicates business-logic intent
@RepositoryDataMarks a DAO; wraps DB exceptions in Spring's DataAccessException
@ControllerPresentationMarks an MVC controller returning views
@RestControllerPresentation= @Controller + @ResponseBody; returns JSON/XML directly
@ConfigurationConfigClass contains @Bean factory methods
@BeanConfigDeclares a single bean inside a @Configuration class

Dependency injection

AnnotationPurpose
@AutowiredInject a bean by type (prefer constructor injection over field injection)
@Qualifier("name")Resolve ambiguity when multiple beans of the same type exist
@PrimaryMark a bean as the default when multiple candidates exist
@Value("${key}")Inject a single config property as a field
@LazyInitialize the bean only when first requested (not at startup)
@Scope("prototype")Create a new bean instance for each injection point

Web / MVC

AnnotationPurpose
@RequestMappingBase URL prefix for a controller class or a handler method
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMappingHTTP-method-specific shorthand for @RequestMapping
@PathVariableBind a URI template variable {id} to a method parameter
@RequestParamBind a query string parameter to a method parameter
@RequestBodyDeserialize the HTTP request body (JSON → Java)
@ResponseBodySerialize the return value to the HTTP response body
@ResponseStatusSet the default HTTP status code for the method
@CrossOriginEnable CORS for a controller or endpoint
@ExceptionHandlerHandle a specific exception inside a controller or @ControllerAdvice
@ControllerAdvice / @RestControllerAdviceGlobal exception handling / model augmentation for all controllers

JPA / persistence

AnnotationPurpose
@EntityMarks a class as a JPA entity mapped to a database table
@Table(name="...")Customize the mapped table name
@IdMarks the primary key field
@GeneratedValueConfigure primary key generation strategy (IDENTITY, SEQUENCE, AUTO)
@ColumnCustomize column name, nullable, unique, length constraints
@OneToMany / @ManyToOne / @OneToOne / @ManyToManyDeclare entity relationships
@JoinColumnSpecify the foreign key column for a relationship
@TransactionalWrap a method or class in a database transaction
@QueryProvide a custom JPQL or native SQL query on a repository method
17 — How it all connects

Component correlation

All components are wired together by the IoC container following a strict unidirectional dependency rule. Understanding this chain is the foundation of maintainable Spring Boot applications.

  1. Client sends an HTTP request. Spring Security's filter chain authenticates and authorizes it before any controller code runs.
  2. DispatcherServlet receives the request and routes it to the matching @RestController method based on @RequestMapping.
  3. Controller validates input (@Valid) and delegates to a @Service. It never accesses the database directly.
  4. Service applies business rules, manages transactions (@Transactional), and calls one or more @Repository interfaces.
  5. Repository runs SQL via JPA/Hibernate against the DataSource. Spring Data generates the implementation; you only write the interface.
  6. The result travels back up the chain. The controller returns a DTO; Jackson serializes it to JSON; the response goes to the client.
  7. If any layer throws an exception, @ControllerAdvice intercepts it and returns a consistent ErrorResponse JSON — no stack trace exposed to the client.
The dependency direction is always downward: Controller → Service → Repository → Database. Never skip a layer. Never let a Repository contain business logic. Never let a Controller talk to a Repository directly.
Pattern 01

Constructor injection

Always inject dependencies through the constructor, not @Autowired on fields. Easier to test, dependencies are explicit and final.

Pattern 02

Never expose entities

Always map @Entity objects to DTOs before returning from a controller. Entities carry Hibernate proxies and lazy-load traps.

Pattern 03

One @ControllerAdvice

Keep a single global exception handler class. It becomes the API's error contract — a consistent shape every consumer can rely on.