Showing posts with label Spring Framework. Show all posts
Showing posts with label Spring Framework. Show all posts

Spring Boot All Annotations with Examples: Complete Guide (2026)

Spring Boot Annotations: Complete Guide with Examples (Beginner to Advanced) (2026)

Spring Boot Annotations: Complete Guide with Real-World Examples (Beginner to Advanced)

Spring Boot has made Java backend development much easier by reducing configuration and allowing developers to focus on business logic. Whether you are building a simple REST API or a microservices-based application, annotations are part of everyday Spring Boot development.

You have probably used annotations like @SpringBootApplication, @RestController, @Service, @Autowired, and @Transactional. But understanding what actually happens behind them is what helps you become a better backend developer.

For example: how does Spring find a service class? How does @RestController convert a Java object into JSON? What actually happens inside @Transactional? Why does @Cacheable sometimes stop working when called from another method in the same class? These are the questions that separate "using Spring Boot" from "understanding Spring Boot".

What you will build: a complete Product CRUD REST API using Java 17, Spring Boot 3, Spring Data JPA, and MySQL — annotated step by step, with validation, exception handling, and testing notes.

What Are Spring Boot Annotations?

Annotations in Java are metadata that provide additional information about classes, methods, fields, parameters, and other program elements. Spring uses annotations to understand how application components should be configured and managed — controllers, services, repositories, configuration classes, database entities, and more.

Consider this simple example:

@RestController
public class ProductController {

    @GetMapping("/products")
    public String getProducts() {
        return "Product list";
    }
}

Here:

  • @RestController tells Spring that this class handles REST API requests.
  • @GetMapping maps HTTP GET requests to the method.
  • Spring detects the controller through component scanning when its package is included in the scan.

Instead of manually configuring every component and request mapping, Spring processes the annotation metadata and handles much of the configuration for us.

How Do Spring Annotations Work Internally?

Spring manages application objects through a container called the ApplicationContext. Objects managed by this container are called Spring Beans. When the application starts, Spring processes configuration metadata, scans configured packages, registers beans, resolves their dependencies, and prepares the application context.

Spring Boot application startup and dependency injection flow SpringApplication.run triggers component scanning, bean registration, dependency injection, and finally the application is ready. SpringApplication.run() Component Scanning Bean Registration Dependency Injection Application Ready

Spring Boot startup: scan, register, inject, then serve traffic.

The important point is that annotations do not execute application logic by themselves. Spring interprets them and applies the relevant framework behavior.

Spring Boot Application Annotations

@SpringBootApplication

The main annotation used to start a Spring Boot application.

@SpringBootApplication
public class ProductApplication {

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

@SpringBootApplication combines three important annotations:

  • @SpringBootConfiguration — identifies the class as a source of Spring configuration.
  • @EnableAutoConfiguration — enables Spring Boot's auto-configuration mechanism.
  • @ComponentScan — scans for components in the configured package and its subpackages by default.

Real-world example: When you add Spring Web and Spring Data JPA dependencies, Spring Boot can automatically configure many required components based on those dependencies and the application configuration.

Important: Keep the main application class in a root package above your controllers, services, and repositories. Otherwise, component scanning may not find all your classes.

@Configuration

Used to declare a class that provides Spring bean configuration.

@Configuration
public class ApplicationConfig {

    @Bean
    public Clock applicationClock() {
        return Clock.systemUTC();
    }
}

Spring processes this configuration class and registers the object returned by the @Bean method.

@Bean

Used on a method inside a configuration class to explicitly register the returned object as a Spring Bean.

@Configuration
public class AppConfig {

    @Bean
    public RestClient restClient() {
        return RestClient.builder().build();
    }
}

Use @Bean when you want to configure an object manually — especially when its class belongs to an external library that you cannot annotate with @Component.

@ComponentScan

Instructs Spring to scan packages for classes annotated with component stereotypes such as @Component, @Service, @Repository, and @Controller.

@Configuration
@ComponentScan(basePackages = "com.example.product")
public class ProductConfiguration {
}

Most Spring Boot applications do not need an explicit @ComponentScan, because @SpringBootApplication already includes component scanning.

Spring Core and Dependency Injection Annotations

Dependency Injection (DI) is one of the most important concepts in Spring. Instead of creating dependent objects manually, a class receives the objects it needs from the Spring container.

For example, a ProductService needs a ProductRepository. Rather than creating the repository with new, we allow Spring to inject it.

@Component

Marks a class as a Spring-managed component.

@Component
public class EmailFormatter {

    public String format(String message) {
        return "[EMAIL] " + message;
    }
}

Spring detects the class during component scanning and registers it as a bean. Use it for general-purpose helper classes that do not specifically belong to the service, repository, or controller layers.

@Service

Marks a class as a service-layer component. It is a specialized form of @Component.

@Service
public class PaymentService {

    public String processPayment() {
        return "Payment processed successfully";
    }
}

Real-world example: A PaymentService may validate a transaction, check limits, call a payment provider, and update transaction status.

@Repository

Marks a class as a persistence-layer component. Spring also provides exception translation support for repository beans when applicable.

@Repository
public class ProductRepositoryCustom {

    public void saveProduct(Product product) {
        // Custom database operation
    }
}

For Spring Data JPA interfaces, extending JpaRepository is usually enough — Spring Data creates the repository implementation.

@Autowired

Injects a dependency managed by Spring.

@Service
public class OrderService {

    @Autowired
    private ProductRepository productRepository;
}

Although field injection works, constructor injection is generally preferred because it makes dependencies explicit and allows fields to be final.

@Service
public class OrderService {

    private final ProductRepository productRepository;

    public OrderService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
}

When a class has only one constructor, Spring can use it for dependency injection without requiring @Autowired.

@Qualifier

Used when multiple beans implement the same interface and you need to specify which one should be injected.

public interface PaymentProvider {
    String pay();
}

@Component("upiProvider")
class UpiProvider implements PaymentProvider {
    public String pay() {
        return "UPI payment";
    }
}

@Component("cardProvider")
class CardProvider implements PaymentProvider {
    public String pay() {
        return "Card payment";
    }
}

@Service
class PaymentService {

    private final PaymentProvider provider;

    PaymentService(@Qualifier("upiProvider")
                   PaymentProvider provider) {
        this.provider = provider;
    }
}

Real-world use case: Selecting between UPI, card, bank transfer, or different vendor integrations.

@Primary

Defines a preferred bean when multiple candidates of the same type exist.

@Component
@Primary
public class DefaultPaymentProvider implements PaymentProvider {
    public String pay() {
        return "Default payment provider";
    }
}

Use @Qualifier when a particular injection point needs a specific bean. Use @Primary to establish the default candidate.

@Lazy

Delays bean initialization until the bean is first required, subject to how it is configured and injected.

@Component
@Lazy
public class ReportGenerator {

    public ReportGenerator() {
        System.out.println("ReportGenerator initialized");
    }
}

Use it for expensive components that are not needed during normal application startup. Avoid using it blindly to hide slow initialization or circular dependency problems.

@Scope

Defines the lifecycle scope of a Spring Bean.

@Component
@Scope("prototype")
public class ReportRequest {
}

Common scopes include:

Scope Meaning
singleton One bean instance per bean definition per Spring container. Default scope.
prototype A new instance is created when the container is asked for the bean.
request One bean instance per HTTP request in a web application.
session One bean instance per HTTP session.
application One bean instance per ServletContext.

Remember that Spring does not automatically manage the complete destruction lifecycle of prototype beans after handing them to the caller.

Spring MVC and REST API Annotations

These annotations are used to expose backend functionality through HTTP APIs.

@RestController

Used to create REST controllers. It combines @Controller and @ResponseBody.

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

    @GetMapping
    public String getProducts() {
        return "All products";
    }
}

Return values are written to the HTTP response body, commonly as JSON when returning Java objects and the appropriate message converter is configured.

@Controller

Used in Spring MVC applications, especially when a method returns a view name.

@Controller
public class HomeController {

    @GetMapping("/home")
    public String home() {
        return "home";
    }
}

Here, home is treated as a view name when view resolution is configured. It is not automatically returned as a REST response body.

@RequestMapping

Maps HTTP requests to a controller or a handler method. It can define a path, HTTP method, headers, parameters, and content types.

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

    @RequestMapping(
        value = "/status",
        method = RequestMethod.GET
    )
    public String status() {
        return "Product API is running";
    }
}

@GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping

Shortcuts for @RequestMapping with a fixed HTTP method.

@GetMapping("/{id}")
public ProductResponse getProduct(@PathVariable Long id) {
    return productService.getProduct(id);
}

@PostMapping
public ProductResponse createProduct(
        @Valid @RequestBody ProductRequest request) {
    return productService.createProduct(request);
}

@PutMapping("/{id}")
public ProductResponse updateProduct(
        @PathVariable Long id,
        @Valid @RequestBody ProductRequest request) {
    return productService.updateProduct(id, request);
}

@PatchMapping("/{id}/price")
public ProductResponse updatePrice(
        @PathVariable Long id,
        @RequestParam BigDecimal price) {
    return productService.updatePrice(id, price);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(
        @PathVariable Long id) {
    productService.deleteProduct(id);
    return ResponseEntity.noContent().build();
}

Use @PutMapping for complete replacement of a resource representation, and @PatchMapping for partial updates.

Request Parameter and Body Annotations

@PathVariable

Reads a value from the URL path.

@GetMapping("/{id}")
public String getProduct(@PathVariable Long id) {
    return "Product ID: " + id;
}

Example URL: GET /api/products/101

@RequestParam

Reads query parameters from the URL.

@GetMapping("/search")
public String search(
        @RequestParam String name,
        @RequestParam(defaultValue = "0") int page) {
    return "Searching: " + name + ", page: " + page;
}

Example URL: GET /api/products/search?name=Phone&page=0

@RequestBody

Converts the HTTP request body into a Java object using an appropriate HTTP message converter.

@PostMapping
public ProductResponse create(
        @RequestBody ProductRequest request) {
    return productService.createProduct(request);
}

@RequestHeader

Reads an HTTP header from the incoming request.

@GetMapping("/profile")
public String profile(
        @RequestHeader("X-Request-Id") String requestId) {
    return "Request ID: " + requestId;
}

@CookieValue

Reads a cookie value from an HTTP request.

@GetMapping("/preferences")
public String preferences(
        @CookieValue(name = "theme", required = false)
        String theme) {
    return "Theme: " + theme;
}

@ResponseStatus

Assigns an HTTP status code to a controller response or an exception type.

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public ProductResponse createProduct(
        @RequestBody ProductRequest request) {
    return productService.createProduct(request);
}

@CrossOrigin

Enables cross-origin requests for a controller or handler method.

@RestController
@CrossOrigin(origins = "https://example.com")
@RequestMapping("/api/products")
public class ProductController {
}
Security note: For production systems, configure allowed origins, methods, and headers deliberately. Avoid using unrestricted origins with credentials.

Spring Data JPA and Hibernate Annotations

Spring Data JPA simplifies database access, while Jakarta Persistence annotations define how Java objects map to database tables.

@Entity and @Table

@Entity
@Table(name = "products")
public class Product {
    // Entity fields
}

@Entity marks a Java class as a JPA entity. @Table defines the database table associated with the entity.

@Id and @GeneratedValue

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

@Id marks the primary key. @GeneratedValue defines the primary key generation strategy. Common strategies include IDENTITY, SEQUENCE, TABLE, and AUTO.

@Column

@Column(
    name = "product_name",
    nullable = false,
    length = 150
)
private String name;

Defines mapping details for a database column.

@Transient

@Transient
private String displayMessage;

Marks a field that should not be persisted as a database column.

@Enumerated

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ProductStatus status;

Using STRING is generally safer than ORDINAL, because changing enum order will not silently change the meaning of stored numeric values.

@OneToOne, @OneToMany, @ManyToOne, @ManyToMany

@OneToOne
@JoinColumn(name = "profile_id")
private ProductProfile profile;

@OneToMany(mappedBy = "product")
private List<ProductImage> images;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;

@ManyToMany
@JoinTable(
    name = "product_tags",
    joinColumns = @JoinColumn(name = "product_id"),
    inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags;

For complex business relationships, an explicit join entity is often easier to maintain than a direct many-to-many mapping.

@JoinColumn

@ManyToOne
@JoinColumn(name = "category_id")
private Category category;

Specifies the foreign key column used for an entity relationship.

@Version

@Version
private Long version;

Supports optimistic locking by maintaining a version field. JPA uses the version to detect conflicting updates.

Real-world use case: Preventing two concurrent requests from silently overwriting each other's updates to the same wallet or product record.

Bean Validation Annotations

Validation annotations help check incoming data before business logic processes it. In Spring Boot 3, validation annotations are generally imported from jakarta.validation.

@Valid

Triggers validation of a request object when used with a supported validation integration.

@PostMapping
public ProductResponse create(
        @Valid @RequestBody ProductRequest request) {
    return productService.createProduct(request);
}

Common Validation Annotations

Annotation What it checks Example
@NotNull Value is not null @NotNull Long categoryId;
@NotBlank String is not null and contains at least one non-whitespace character @NotBlank String name;
@NotEmpty String, collection, map, or array is not null and not empty @NotEmpty List<String> tags;
@Size Size is within a range @Size(min = 2, max = 150) String name;
@Min / @Max Numeric value within bounds @Min(1) @Max(100000) int quantity;
@Positive Numeric value is greater than zero @Positive BigDecimal price;
@Email String matches email format constraints @Email String email;

Validation annotations improve input checking, but they do not replace business validation. For example, checking whether a product SKU is unique still requires application or database logic.

Exception Handling Annotations

Centralized exception handling keeps controllers clean and helps return consistent API error responses.

@ExceptionHandler

Handles a particular exception inside a controller or advice class.

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<String> handleNotFound(
        ProductNotFoundException ex) {

    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ex.getMessage());
}

@ControllerAdvice

Shares exception handling and other controller advice across multiple controllers.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleNotFound(
            ProductNotFoundException ex) {

        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(ex.getMessage());
    }
}

@RestControllerAdvice

Combines @ControllerAdvice and @ResponseBody. Useful for returning JSON error responses from REST APIs.

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleBadRequest(
            IllegalArgumentException ex) {

        Map<String, String> response = Map.of(
                "error", "BAD_REQUEST",
                "message", ex.getMessage()
        );

        return ResponseEntity.badRequest().body(response);
    }
}

Transaction Management Annotations

@Transactional

Defines a transactional boundary around a method or class. Spring typically manages transactions through a proxy and a configured transaction manager.

@Service
public class TransferService {

    @Transactional
    public void transfer(Long fromId, Long toId,
                         BigDecimal amount) {
        // Debit source account
        // Credit destination account
        // Save both changes
    }
}

If a transaction rolls back, database changes made within that transaction are rolled back according to the transaction manager and rollback rules.

By default, Spring rolls back on unchecked exceptions and errors. Checked exceptions do not trigger rollback by default unless configured, for example with rollbackFor.

@Transactional(rollbackFor = Exception.class)
public void processPayment() throws Exception {
    // Transactional business operation
}
Production considerations: Keep transactions as short as possible. Avoid slow external API calls inside a database transaction. Understand propagation and isolation before changing defaults. Remember that self-invocation does not normally trigger the transactional interceptor. And do not assume a database transaction automatically includes Redis, Kafka, or an external payment provider.

@EnableTransactionManagement

Enables annotation-driven transaction management in configurations where it is not already enabled by the framework setup.

@Configuration
@EnableTransactionManagement
public class TransactionConfig {
}

Spring Boot applications using typical auto-configuration usually do not need to declare this manually.

Caching Annotations

Caching helps reduce repeated database calls and improve response times for data that can safely be reused.

@EnableCaching

@Configuration
@EnableCaching
public class CacheConfig {
}

Enables Spring's annotation-driven cache management.

@Cacheable, @CachePut, @CacheEvict

Annotation Purpose Executes method?
@Cacheable Read and cache result Only on cache miss
@CachePut Update cached result Always
@CacheEvict Remove cached result Always
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
    return repository.findById(id)
            .map(this::toResponse)
            .orElseThrow();
}

@CachePut(value = "products", key = "#id")
public ProductResponse updateProduct(Long id,
                                     ProductRequest request) {
    // Update product and return updated response
}

@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
    repository.deleteById(id);
}

For production applications, consider cache/database consistency, transaction timing, TTL, serialization, invalidation, and cache failure handling. A cache annotation alone does not solve distributed consistency.

Scheduling and Asynchronous Annotations

@EnableScheduling and @Scheduled

@SpringBootApplication
@EnableScheduling
public class ProductApplication {
}
@Component
public class ReportScheduler {

    @Scheduled(cron = "0 0 23 * * *")
    public void generateDailyReport() {
        System.out.println("Generating daily report");
    }
}

The cron expression above schedules execution at 11 PM according to the configured scheduler time zone. In distributed deployments, every application instance may execute the same task unless you coordinate execution with a distributed lock or another scheduling mechanism.

@EnableAsync and @Async

@Configuration
@EnableAsync
public class AsyncConfig {
}
@Service
public class NotificationService {

    @Async
    public CompletableFuture<Void> sendEmail(String email) {
        // Send email
        return CompletableFuture.completedFuture(null);
    }
}

For production, configure the executor, thread pool size, queue capacity, and rejection policy. Handle exceptions and understand that proxy-based async calls are not triggered through self-invocation.

Spring Security Annotations

@EnableMethodSecurity

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}

Enables method-level security annotations.

@PreAuthorize

@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long id) {
    // Delete product
}

Checks authorization before a method executes.

@PostAuthorize

@PostAuthorize("returnObject.owner == authentication.name")
public ProductResponse getMyProduct(Long id) {
    // Return product
}

Checks authorization after a method executes. It can be useful when access depends on the returned object, but should be used carefully because the method has already executed.

@Secured

@Secured("ROLE_ADMIN")
public void updateConfiguration() {
    // Restricted operation
}

Method-level security should complement, not replace, appropriate HTTP security rules and resource ownership checks.

Testing Annotations

@SpringBootTest

@SpringBootTest
class ProductApplicationTest {

    @Test
    void applicationStarts() {
    }
}

Loads the full Spring application context for an integration test.

@WebMvcTest

@WebMvcTest(ProductController.class)
class ProductControllerTest {
}

Loads a focused Spring MVC test slice, commonly used to test controllers without loading the complete application.

@DataJpaTest

@DataJpaTest
class ProductRepositoryTest {
}

Loads a JPA-focused test slice for testing repository and persistence behavior.

@MockBean vs @MockitoBean

Spring Boot versions have historically provided @MockBean for registering Mockito mocks in the application context. In newer Spring Framework testing APIs, @MockitoBean is available. Prefer the annotation supported by the Spring Boot and Spring Framework versions used by your project.

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @MockitoBean
    private ProductService productService;
}

@BeforeEach and @AfterEach

These are JUnit Jupiter annotations, not Spring-specific annotations. They run setup and cleanup methods before and after each test method.

@BeforeEach
void setUp() {
    // Prepare test data
}

@AfterEach
void tearDown() {
    // Clean up
}

Complete Spring Boot CRUD Project: Step by Step

Now let's put these annotations into practice. We will create a Product Management REST API using Java 17, Spring Boot 3, Spring Data JPA, and MySQL.

Our API will support:

  • Create a product
  • Get all products
  • Get a product by ID
  • Update a product
  • Delete a product
  • Validate incoming request data
  • Handle application exceptions

Project Structure

product-service/
|
|-- src/main/java/com/example/product/
|   |
|   |-- ProductApplication.java
|   |
|   |-- controller/
|   |   |-- ProductController.java
|   |
|   |-- service/
|   |   |-- ProductService.java
|   |
|   |-- repository/
|   |   |-- ProductRepository.java
|   |
|   |-- entity/
|   |   |-- Product.java
|   |
|   |-- dto/
|   |   |-- ProductRequest.java
|   |   |-- ProductResponse.java
|   |
|   |-- exception/
|       |-- ProductNotFoundException.java
|       |-- GlobalExceptionHandler.java
|
|-- src/main/resources/
|   |-- application.yml
|
|-- build.gradle

Step 1: Gradle Configuration

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.5.5'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '1.0.0'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

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

    runtimeOnly 'com.mysql:mysql-connector-j'

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

tasks.named('test') {
    useJUnitPlatform()
}

Step 2: Configure MySQL

CREATE DATABASE product_db;
server:
  port: 8081

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/product_db
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

Production note: Do not store database credentials directly in a committed configuration file. Use environment variables or a secrets manager. Use managed database migrations such as Flyway or Liquibase instead of ddl-auto: update in production.

Step 3: Main Application Class

package com.example.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductApplication {

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

Step 4: Product Entity

package com.example.product.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class Product {

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

    @Column(nullable = false, length = 150)
    private String name;

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer quantity;

    public Product() {
    }

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

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
}

Step 5: DTOs

package com.example.product.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;

public record ProductRequest(

    @NotBlank(message = "Product name is required")
    @Size(max = 150, message = "Name must not exceed 150 characters")
    String name,

    @NotNull(message = "Price is required")
    @Positive(message = "Price must be greater than zero")
    BigDecimal price,

    @NotNull(message = "Quantity is required")
    @PositiveOrZero(message = "Quantity cannot be negative")
    Integer quantity

) {}
package com.example.product.dto;

import java.math.BigDecimal;

public record ProductResponse(
    Long id,
    String name,
    BigDecimal price,
    Integer quantity
) {}

Step 6: Repository

package com.example.product.repository;

import com.example.product.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository
        extends JpaRepository<Product, Long> {
}

Spring Data JPA creates the repository implementation at runtime. You do not need to write basic CRUD queries manually.

Step 7: Custom Exception

package com.example.product.exception;

public class ProductNotFoundException
        extends RuntimeException {

    public ProductNotFoundException(Long id) {
        super("Product not found with ID: " + id);
    }
}

Step 8: Service Layer

package com.example.product.service;

import com.example.product.dto.ProductRequest;
import com.example.product.dto.ProductResponse;
import com.example.product.entity.Product;
import com.example.product.exception.ProductNotFoundException;
import com.example.product.repository.ProductRepository;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
public class ProductService {

    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public ProductResponse create(ProductRequest request) {
        Product product = new Product(
                request.name(),
                request.price(),
                request.quantity()
        );

        return toResponse(repository.save(product));
    }

    public List<ProductResponse> getAll() {
        return repository.findAll()
                .stream()
                .map(this::toResponse)
                .toList();
    }

    public ProductResponse getById(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(() ->
                        new ProductNotFoundException(id));

        return toResponse(product);
    }

    @Transactional
    public ProductResponse update(Long id,
                                  ProductRequest request) {
        Product product = repository.findById(id)
                .orElseThrow(() ->
                        new ProductNotFoundException(id));

        product.setName(request.name());
        product.setPrice(request.price());
        product.setQuantity(request.quantity());

        return toResponse(repository.save(product));
    }

    @Transactional
    public void delete(Long id) {
        Product product = repository.findById(id)
                .orElseThrow(() ->
                        new ProductNotFoundException(id));

        repository.delete(product);
    }

    private ProductResponse toResponse(Product product) {
        return new ProductResponse(
                product.getId(),
                product.getName(),
                product.getPrice(),
                product.getQuantity()
        );
    }
}

Notice how the service uses constructor injection rather than field injection. It also uses @Transactional to define transaction boundaries for write operations.

Step 9: REST Controller

package com.example.product.controller;

import com.example.product.dto.ProductRequest;
import com.example.product.dto.ProductResponse;
import com.example.product.service.ProductService;

import jakarta.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

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

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ProductResponse create(
            @Valid @RequestBody ProductRequest request) {
        return productService.create(request);
    }

    @GetMapping
    public List<ProductResponse> getAll() {
        return productService.getAll();
    }

    @GetMapping("/{id}")
    public ProductResponse getById(
            @PathVariable Long id) {
        return productService.getById(id);
    }

    @PutMapping("/{id}")
    public ProductResponse update(
            @PathVariable Long id,
            @Valid @RequestBody ProductRequest request) {
        return productService.update(id, request);
    }

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

Step 10: Global Exception Handling

package com.example.product.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<Map<String, String>> handleNotFound(
            ProductNotFoundException ex) {

        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of(
                        "error", "NOT_FOUND",
                        "message", ex.getMessage()
                ));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(
            MethodArgumentNotValidException ex) {

        String message = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": "
                        + error.getDefaultMessage())
                .collect(Collectors.joining(", "));

        return ResponseEntity.badRequest()
                .body(Map.of(
                        "error", "VALIDATION_FAILED",
                        "message", message
                ));
    }
}

Step 11: Run the Application

./gradlew bootRun

On Windows:

gradlew.bat bootRun

When the application starts successfully, it will run on port 8081.

Test the API Using Postman

Create a Product

Method: POST — http://localhost:8081/api/products

{
  "name": "Laptop",
  "price": 65000.00,
  "quantity": 10
}

Expected response: HTTP 201 Created.

{
  "id": 1,
  "name": "Laptop",
  "price": 65000.00,
  "quantity": 10
}

Get All Products

Method: GET — http://localhost:8081/api/products

Get Product by ID

Method: GET — http://localhost:8081/api/products/1

Update Product

Method: PUT — http://localhost:8081/api/products/1

{
  "name": "Gaming Laptop",
  "price": 75000.00,
  "quantity": 5
}

Delete Product

Method: DELETE — http://localhost:8081/api/products/1

Expected response: HTTP 204 No Content.

Test Validation

{
  "name": "",
  "price": -100,
  "quantity": -5
}

Spring validation should reject the request and return HTTP 400 Bad Request with validation details.

How All These Annotations Work Together

Spring Boot REST API architecture with controller, service, repository, and MySQL A request flows from client to controller, then to service, then to repository, and finally to the MySQL database. Validation happens at the controller boundary and transactions wrap service calls. Spring Boot REST API Request Flow Client Postman / UI Controller @RestController Service @Service Repository JpaRepository MySQL Database products table @Valid checks request @Transactional

Each layer has a specific responsibility, and annotations tell Spring how to wire them together.

  1. The client sends an HTTP request.
  2. @RestController receives the request.
  3. @RequestBody converts JSON into a Java DTO.
  4. @Valid triggers request validation.
  5. The controller calls the service.
  6. @Service identifies the business logic component.
  7. @Transactional manages the database transaction.
  8. JpaRepository performs database operations.
  9. Spring converts the response object into JSON.

Common Spring Boot Annotation Mistakes

1. Using @Component Everywhere

Technically, @Service, @Repository, and @Controller are specialized component stereotypes. Prefer the annotation that communicates the class's responsibility.

2. Using Field Injection Everywhere

Field injection hides dependencies and makes isolated unit testing harder. Prefer constructor injection for required dependencies.

3. Forgetting @Valid

Defining validation constraints on a DTO does not automatically validate every incoming request. Trigger validation at the appropriate boundary, such as with @Valid.

4. Incorrect Package Structure

If the main application class is placed in an unrelated package, component scanning may miss controllers, services, or repositories. Keep application packages under a common root or configure scanning explicitly.

5. Expecting @Transactional to Work During Self-Invocation

In the default proxy-based setup, calling a transactional method from another method in the same object bypasses the Spring proxy. Put the transactional operation behind a separate Spring bean or use an appropriate transaction design.

6. Ignoring Lazy Loading

JPA relationships can use lazy loading. Accessing unloaded data after the persistence context is closed can cause errors. Design fetch strategies and DTO mapping deliberately.

7. Using @Scheduled in Multiple Instances Without Coordination

Every application instance may run the same scheduled method. Use distributed coordination when a task must execute only once across the deployment.

8. Assuming @Async Makes Everything Faster

Asynchronous execution consumes threads and resources. Configure the executor and avoid unbounded queues or uncontrolled thread creation.

Spring Boot Annotation Comparison

Annotation Purpose Typical usage
@Component General Spring bean Helper component
@Service Business logic bean PaymentService
@Repository Persistence component Custom DAO
@Controller MVC controller View-based web application
@RestController REST controller JSON APIs
@Configuration Configuration class ApplicationConfig
@Bean Registers returned object Custom ObjectMapper
@Autowired Dependency injection Constructor or field injection
@Qualifier Selects a bean Multiple implementations
@Transactional Transaction boundary Database operations
@Cacheable Reads and populates cache Product lookup
@Scheduled Scheduled execution Daily report
@Async Asynchronous execution Email notification
@PreAuthorize Method authorization Admin operation
@RestControllerAdvice Global REST advice Exception handling

Best Practices for Using Spring Boot Annotations

  • Use constructor injection for mandatory dependencies.
  • Keep controllers focused on HTTP handling, not business logic.
  • Keep business rules inside service-layer components.
  • Use DTOs instead of exposing JPA entities directly from APIs.
  • Use @Valid and meaningful validation messages for incoming data.
  • Use @Transactional at suitable service boundaries.
  • Understand proxy behavior before relying on caching, transactions, or asynchronous execution.
  • Use explicit bean qualifiers when multiple implementations exist.
  • Configure security and CORS deliberately.
  • Write unit and integration tests for important business flows.
  • Use database migrations instead of automatic schema updates in production.
  • Do not add annotations without understanding their lifecycle and behavior.

Frequently Asked Interview Questions

1. What is the difference between @Component and @Bean?

@Component marks a class for component scanning. @Bean marks a method whose returned object should be registered in the Spring container.

2. What is the difference between @Service and @Repository?

Both are specialized components. @Service communicates business-layer responsibility. @Repository identifies persistence-layer responsibility and participates in persistence exception translation.

3. What is the difference between @Controller and @RestController?

@Controller is commonly used for MVC view handling. @RestController combines @Controller and @ResponseBody, making handler return values response bodies.

4. What is the difference between @Autowired and @Qualifier?

@Autowired resolves and injects a dependency. @Qualifier helps select a particular bean when multiple candidates are available.

5. What is the purpose of @Primary?

It marks a preferred bean when multiple candidates of the same type are available for injection.

6. What is the difference between @PathVariable and @RequestParam?

@PathVariable reads values from the URL path. @RequestParam reads query parameters or form parameters.

7. What is the difference between @RequestBody and @ResponseBody?

@RequestBody converts an incoming request body into an object. @ResponseBody writes a method's return value into the HTTP response body.

8. What is the purpose of @Transactional?

It defines transaction boundaries so database operations can commit or roll back according to transaction rules.

9. Why might @Transactional not work when calling a method from the same class?

In the default proxy-based transaction model, self-invocation bypasses the Spring proxy, so the transaction interceptor is not applied to that internal call.

10. What is the difference between @Cacheable and @CachePut?

@Cacheable can skip method execution when a matching cached result exists. @CachePut always executes the method and updates the cache with the result.

11. What is the difference between @RestControllerAdvice and @ControllerAdvice?

@RestControllerAdvice includes response-body behavior, making it convenient for centralized REST JSON responses. @ControllerAdvice is more general and can also support view-based MVC applications.

12. What is the purpose of @Version in JPA?

It supports optimistic locking and helps detect concurrent updates to the same entity.

13. What is the difference between @OneToMany and @ManyToOne?

@OneToMany represents one entity associated with multiple entities. @ManyToOne represents multiple entities associated with one entity.

14. What is the purpose of @Scheduled?

It schedules method execution using fixed delay, fixed rate, or cron expressions.

15. Does @Async automatically create a new thread for every call?

Not necessarily. Spring delegates asynchronous execution to a configured task executor. Its thread pool and queue behavior determine how tasks are executed.

Your Spring Boot Annotation Checklist

Before shipping a Spring Boot service, ask:

  • Are controllers thin? Business logic belongs in @Service classes.
  • Are DTOs used instead of entities? Exposing entities directly leaks internal schema.
  • Is validation triggered at the API boundary? Use @Valid on request bodies.
  • Are transactions at the correct boundary? Prefer service methods, not controllers.
  • Are multiple implementations resolved deliberately? Use @Qualifier or @Primary.
  • Is caching behavior understood? Remember self-invocation bypasses the proxy.
  • Are security rules explicit? Use @PreAuthorize where appropriate.
  • Is the main class in the root package? Component scanning depends on it.
  • Are scheduled tasks safe across instances? Use a distributed lock when needed.
  • Are important flows tested? Unit tests for services, integration tests for APIs.

Start small. Pick one layer — controllers, or services — and apply these practices until they become habits.

Practice Assignments

  1. Create a Category entity and connect it to Product using @ManyToOne and @OneToMany.
  2. Add pagination and sorting using Spring Data JPA.
  3. Add a custom validation annotation for product SKU format.
  4. Implement global exception handling with a standard error response.
  5. Add Redis caching using @Cacheable, @CachePut, and @CacheEvict.
  6. Protect product deletion using @PreAuthorize.
  7. Create a scheduled job that generates a daily product report.
  8. Write controller tests using @WebMvcTest and service tests using Mockito.
  9. Add optimistic locking with @Version and test concurrent updates.
  10. Build a small Order and Payment microservices project using these concepts.

Conclusion

Spring Boot annotations are more than shortcuts that reduce code. They are a way to describe application behavior so that the Spring Framework can manage components, dependencies, HTTP requests, persistence, validation, transactions, and security.

I'll be honest: when I started with Spring Boot, I copied annotations from tutorials without understanding why they were there. It worked — until it didn't. The day a @Transactional method silently failed to roll back, or a @Cacheable call inside the same class was ignored, was the day I realized annotations are not magic. They are instructions for a proxy that only exists if you call through it.

If you are a fresher, start with @SpringBootApplication, @Component, @Service, @Repository, @RestController, and the basic request-mapping annotations. Once those feel natural, move toward JPA relationships, validation, transactions, caching, and security.

If you are an experienced developer, focus on understanding the behavior behind the annotations. Proxy-based execution, transaction boundaries, lazy loading, cache consistency, concurrency, and method-level security are where production-grade applications are won or lost.

The most important lesson is this: do not memorize annotations — understand what Spring does with them. Build real applications, debug their behavior, and reason through why each annotation is needed. That is what separates developers who use Spring Boot from developers who understand it.

Related topics: Redis Caching in Spring Boot, Chain of Responsibility Design Pattern in Java, Spring Data JPA with MySQL, Spring Security Method-Level Authorization, Java Design Patterns with Real-World Examples, Spring Boot Testing Best Practices


Keep Learning Java and Spring Boot

Enjoyed this tutorial? Explore more practical Java, Spring Boot, Hibernate, microservices, and system design articles on the blog.

Which Spring Boot annotation caused you the most confusion when you started? Share your experience in the comments.