Spring Boot is the industry standard for building Java backend servers and REST APIs. It eliminates boilerplate configuration through auto-configuration, embedded servers (Tomcat), and production-ready features out of the box. Companies like Netflix, Amazon, Atlassian, and thousands of enterprises use Spring Boot to power their backends.


Step 1 — Project Setup with Spring Initializr

Go to start.spring.io and configure your project. Select these dependencies for a full REST API with database.

Essential Dependencies to Select

  • Spring Web — Includes Spring MVC, embedded Tomcat, REST support.
  • Spring Data JPA — ORM layer using Hibernate. Simplifies database operations.
  • PostgreSQL Driver (or H2 for dev) — JDBC driver for your database.
  • Spring Boot Validation — Bean Validation (JSR-380) with Hibernate Validator.
  • Spring Security — Authentication and authorization framework.
  • Lombok — Reduces boilerplate: @Getter, @Setter, @Builder, @Slf4j etc.
  • Spring Boot Actuator — Production monitoring endpoints (/health, /metrics).
pom.xml (key dependencies)xml
<dependencies>
    <!-- Web: Spring MVC + Tomcat -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

    <!-- PostgreSQL JDBC driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Bean Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

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

    <!-- Lombok (code generation) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Actuator (monitoring) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>
src/main/resources/application.ymlyaml
server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp_db
    username: postgres
    password: secret
    hikari:                    # HikariCP connection pool (Spring Boot default)
      pool-name: MainPool
      maximum-pool-size: 10    # max concurrent DB connections
      minimum-idle: 2
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate       # 'create' for dev, 'validate' for prod, NEVER 'update' in prod
    show-sql: true             # log SQL queries (disable in prod)
    open-in-view: false        # always set false — avoids lazy loading in views
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.PostgreSQLDialect

  jackson:
    default-property-inclusion: non_null  # don't serialize null fields
    date-format: yyyy-MM-dd HH:mm:ss

logging:
  level:
    org.springframework.web: INFO
    org.hibernate.SQL: DEBUG
    com.yourapp: DEBUG

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics   # actuator endpoints to expose

Step 2 — Project Structure

Recommended package structurebash
src/main/java/com/yourapp/
├── YourAppApplication.java       # @SpringBootApplication entry point
├── controller/
│   └── UserController.java       # REST endpoints: handles HTTP req/res
├── service/
│   ├── UserService.java          # interface
│   └── UserServiceImpl.java      # business logic
├── repository/
│   └── UserRepository.java       # Spring Data JPA interface
├── entity/
│   └── User.java                 # JPA entity (maps to DB table)
├── dto/
│   ├── UserCreateRequest.java    # incoming request body
│   └── UserResponse.java         # outgoing response body
├── exception/
│   ├── ResourceNotFoundException.java
│   └── GlobalExceptionHandler.java
├── security/
│   ├── SecurityConfig.java
│   └── JwtTokenFilter.java
└── config/
    └── BeanConfig.java

Step 3 — The Entry Point

YourAppApplication.javajava
package com.yourapp;

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

// @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
// Auto-configures everything: Tomcat, JPA, Security, Jackson, etc.
@SpringBootApplication
public class YourAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourAppApplication.class, args);
        // Spring Boot starts embedded Tomcat on port 8080
        // Component scan finds all @Component, @Service, @Repository, @Controller beans
    }
}

Step 4 — JPA Entity

entity/User.javajava
package com.yourapp.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = "email", name = "uk_users_email")
    },
    indexes = {
        @Index(columnList = "email", name = "idx_users_email"),
        @Index(columnList = "created_at", name = "idx_users_created")
    }
)
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // DB auto-increment
    private Long id;

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

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

    @Column(nullable = false)
    private String password; // store BCrypt hash, NEVER plaintext

    @Enumerated(EnumType.STRING) // store as 'ACTIVE'/'INACTIVE', not 0/1
    @Column(nullable = false, length = 20)
    private UserStatus status;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // JPA lifecycle callbacks
    @PrePersist
    protected void onCreate() {
        createdAt = updatedAt = LocalDateTime.now();
        if (status == null) status = UserStatus.ACTIVE;
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    public enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }
}

Step 5 — Repository Layer (Spring Data JPA)

repository/UserRepository.javajava
package com.yourapp.repository;

import com.yourapp.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // JpaRepository<Entity, PK type> provides:
    // save(), findById(), findAll(), deleteById(), count(), existsById() etc.

    // --- Derived query methods: Spring generates SQL from method name ---
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
    List<User> findByStatus(User.UserStatus status);
    List<User> findByNameContainingIgnoreCase(String name);
    List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
    List<User> findByStatusOrderByCreatedAtDesc(User.UserStatus status);

    // --- JPQL (object-oriented query language) ---
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
    Optional<User> findActiveUserByEmail(@Param("email") String email,
                                          @Param("status") User.UserStatus status);

    // --- Native SQL query ---
    @Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
           nativeQuery = true)
    List<User> findUsersRegisteredLastWeek();

    // --- Modifying query (UPDATE/DELETE) ---
    @org.springframework.data.jpa.repository.Modifying
    @org.springframework.transaction.annotation.Transactional
    @Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
    int updateUserStatus(@Param("id") Long id, @Param("status") User.UserStatus status);

    // --- Pagination support ---
    // Just change return type to Page<User> and add Pageable parameter
    org.springframework.data.domain.Page<User> findByStatus(
        User.UserStatus status,
        org.springframework.data.domain.Pageable pageable
    );
}

Step 6 — DTOs and Validation

dto/UserCreateRequest.javajava
package com.yourapp.dto;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data // Lombok: generates getters, setters, equals, hashCode, toString
public class UserCreateRequest {

    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(
        regexp = "^(?=.*[A-Z])(?=.*[0-9]).*$",
        message = "Password must contain at least one uppercase letter and one digit"
    )
    private String password;

    @Min(value = 18, message = "Must be at least 18 years old")
    @Max(value = 120, message = "Invalid age")
    private Integer age;
}

// UserResponse.java — what we SEND back (never expose password!)
// Using record for clean immutable DTO
record UserResponse(
    Long id,
    String name,
    String email,
    String status,
    java.time.LocalDateTime createdAt
) {}
// Map from entity to DTO in the service layer

Step 7 — Service Layer

service/UserServiceImpl.javajava
package com.yourapp.service;

import com.yourapp.dto.*;
import com.yourapp.entity.User;
import com.yourapp.exception.ResourceNotFoundException;
import com.yourapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
@RequiredArgsConstructor // Lombok: generates constructor for all final fields
@Slf4j                   // Lombok: generates 'log' field
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public UserResponse createUser(UserCreateRequest request) {
        log.info("Creating user with email: {}", request.getEmail());

        if (userRepository.existsByEmail(request.getEmail())) {
            throw new IllegalArgumentException("Email already in use: " + request.getEmail());
        }

        User user = User.builder()
            .name(request.getName())
            .email(request.getEmail().toLowerCase())
            .password(passwordEncoder.encode(request.getPassword())) // BCrypt hash
            .build();

        User saved = userRepository.save(user);
        log.info("User created with id: {}", saved.getId());
        return toResponse(saved);
    }

    @Override
    @Transactional(readOnly = true) // readOnly=true: performance optimization for reads
    public UserResponse getUserById(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        return toResponse(user);
    }

    @Override
    @Transactional(readOnly = true)
    public List<UserResponse> getAllActiveUsers() {
        return userRepository.findByStatus(User.UserStatus.ACTIVE)
            .stream()
            .map(this::toResponse)
            .toList();
    }

    @Override
    @Transactional
    public UserResponse updateUser(Long id, UserUpdateRequest request) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));

        if (request.getName() != null) user.setName(request.getName());
        // No need to call save() — @Transactional auto-flushes dirty entities
        return toResponse(user);
    }

    @Override
    @Transactional
    public void deleteUser(Long id) {
        if (!userRepository.existsById(id))
            throw new ResourceNotFoundException("User", "id", id);
        userRepository.deleteById(id);
    }

    private UserResponse toResponse(User user) {
        return new UserResponse(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getStatus().name(),
            user.getCreatedAt()
        );
    }
}

Step 8 — REST Controller

controller/UserController.javajava
package com.yourapp.controller;

import com.yourapp.dto.*;
import com.yourapp.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController                    // @Controller + @ResponseBody on every method
@RequestMapping("/api/v1/users")   // base path for all endpoints
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // POST /api/v1/users
    // Creates a new user. Returns 201 Created.
    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @Valid @RequestBody UserCreateRequest request) { // @Valid triggers validation
        UserResponse response = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    // GET /api/v1/users/{id}
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserById(id));
    }

    // GET /api/v1/users?page=0&size=20&sort=createdAt,desc
    @GetMapping
    public ResponseEntity<List<UserResponse>> getAllUsers() {
        return ResponseEntity.ok(userService.getAllActiveUsers());
    }

    // GET /api/v1/users/paged?page=0&size=10
    @GetMapping("/paged")
    public ResponseEntity<Page<UserResponse>> getPagedUsers(
            @PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {
        // Spring auto-parses ?page=0&size=20&sort=name,asc into Pageable
        return ResponseEntity.ok(userService.getPagedUsers(pageable));
    }

    // PUT /api/v1/users/{id}
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateRequest request) {
        return ResponseEntity.ok(userService.updateUser(id, request));
    }

    // PATCH /api/v1/users/{id}/status?status=SUSPENDED
    @PatchMapping("/{id}/status")
    public ResponseEntity<Void> updateStatus(
            @PathVariable Long id,
            @RequestParam User.UserStatus status) {
        userService.updateStatus(id, status);
        return ResponseEntity.noContent().build(); // 204 No Content
    }

    // DELETE /api/v1/users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

Step 9 — Global Exception Handler

exception/GlobalExceptionHandler.javajava
package com.yourapp.exception;

import org.springframework.http.*;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;

// Custom exception
class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, String field, Object value) {
        super(resource + " not found with " + field + " = '" + value + "'");
    }
}

// Standardized error response
record ErrorResponse(
    int status,
    String error,
    String message,
    LocalDateTime timestamp,
    Map<String, String> fieldErrors // for validation failures
) {}

@RestControllerAdvice // intercepts exceptions from ALL controllers
public class GlobalExceptionHandler {

    // Handle @Valid validation failures — 400 Bad Request
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {
        Map<String, String> fieldErrors = new LinkedHashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String field   = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            fieldErrors.put(field, message);
        });
        return ResponseEntity.badRequest().body(new ErrorResponse(
            400, "Validation Failed",
            "Request body has validation errors",
            LocalDateTime.now(),
            fieldErrors
        ));
    }

    // Handle resource not found — 404
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(
            404, "Not Found", ex.getMessage(), LocalDateTime.now(), null
        ));
    }

    // Handle business logic errors — 400
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body(new ErrorResponse(
            400, "Bad Request", ex.getMessage(), LocalDateTime.now(), null
        ));
    }

    // Handle all other unexpected errors — 500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        // Log the full stack trace server-side (never send it to the client!)
        return ResponseEntity.internalServerError().body(new ErrorResponse(
            500, "Internal Server Error",
            "An unexpected error occurred",
            LocalDateTime.now(), null
        ));
    }
}

Step 10 — JWT Security with Spring Security

security/SecurityConfig.javajava
package com.yourapp.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // enables @PreAuthorize annotations
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenFilter jwtTokenFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())   // disable CSRF: we use stateless JWT
            .sessionManagement(s -> s.sessionCreationPolicy(
                SessionCreationPolicy.STATELESS)) // no sessions — JWT is the session
            .authorizeHttpRequests(auth -> auth
                // Public endpoints (no auth needed)
                .requestMatchers(HttpMethod.POST, "/api/v1/auth/login").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/v1/auth/register").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                // Admin-only endpoints
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                // All other requests require authentication
                .anyRequest().authenticated()
            )
            // Add JWT filter BEFORE the default username/password filter
            .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // cost factor 12 (2^12 iterations)
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
security/JwtTokenFilter.javajava
package com.yourapp.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;

@Component
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {

    @Value("${app.jwt.secret}") // from application.yml
    private String jwtSecret;

    @Value("${app.jwt.expiration-ms:86400000}") // 24h default
    private long jwtExpirationMs;

    private final UserDetailsService userDetailsService;

    public JwtTokenFilter(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    // Generate JWT token for login
    public String generateToken(String email) {
        Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
        return Jwts.builder()
            .subject(email)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(key)
            .compact();
    }

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

        String header = req.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
                String email = Jwts.parser()
                    .verifyWith((javax.crypto.SecretKey) key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload()
                    .getSubject();

                UserDetails userDetails = userDetailsService.loadUserByUsername(email);
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);

            } catch (JwtException e) {
                log.warn("Invalid JWT token: {}", e.getMessage());
                // Don't set auth — request will fail at authorization step
            }
        }

        chain.doFilter(req, res);
    }
}