Skip to main content

Spring Boot Development

Spring Boot simplifies Spring application development by providing auto-configuration and production-ready features out of the box.

Introduction

What is Spring Boot?

Spring Boot is an opinionated framework built on top of Spring Framework that eliminates boilerplate configuration and enables rapid application development with sensible defaults.
Key Features:
  • Auto-configuration based on classpath dependencies
  • Standalone applications with embedded servers
  • Production-ready features (metrics, health checks)
  • No XML configuration required
  • Minimal code generation

Spring Boot vs Spring Framework

<!-- web.xml -->
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
</servlet>

<!-- Multiple XML configuration files -->
<!-- Manual dependency management -->
<!-- External application server required -->

Getting Started

Creating a Spring Boot Application

Using web interface:
  1. Visit https://start.spring.io/
  2. Select:
    • Project: Maven/Gradle
    • Language: Java
    • Spring Boot version
    • Dependencies: Web, JPA, etc.
  3. Generate and download
Using CLI:
curl https://start.spring.io/starter.zip \
  -d dependencies=web,data-jpa,mysql \
  -d type=maven-project \
  -d bootVersion=3.2.0 \
  -d groupId=com.example \
  -d artifactId=demo \
  -o demo.zip

Main Application Class

package com.example.demo;

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

@SpringBootApplication
// Equivalent to:
// @Configuration
// @EnableAutoConfiguration
// @ComponentScan
public class DemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Spring Boot Starters

Common Starters

Web Applications

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Includes: Spring MVC, Tomcat, JSON

Data Access

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Includes: Hibernate, JPA, JDBC

Security

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

Testing

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
Includes: JUnit, Mockito, AssertJ

Configuration

Application Properties

# Server Configuration
server.port=8080
server.servlet.context-path=/api

# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Logging
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.file.name=application.log

Profiles

server.port=8080
spring.datasource.url=jdbc:h2:mem:testdb
logging.level.root=DEBUG

Building REST APIs

Basic REST Controller

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User created = userService.save(user);
        return ResponseEntity
            .created(URI.create("/api/users/" + created.getId()))
            .body(created);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody User user) {
        return userService.update(id, user)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        if (userService.delete(id)) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

Request/Response Handling

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

@GetMapping("/users/{id}/posts/{postId}")
public Post getUserPost(
        @PathVariable Long id,
        @PathVariable Long postId) {
    return postService.findByUserAndId(id, postId);
}

Validation

Always validate user input to prevent security vulnerabilities and data corruption.
// Entity with validation
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;
    
    @Email(message = "Email should be valid")
    @NotBlank(message = "Email is required")
    private String email;
    
    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 150, message = "Age must be less than 150")
    private Integer age;
    
    @Pattern(regexp = "^\\+?[0-9]{10,15}$", message = "Invalid phone number")
    private String phone;
    
    // getters and setters
}

// Controller with validation
@RestController
public class UserController {
    
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User created = userService.save(user);
        return ResponseEntity.ok(created);
    }
}

// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errors);
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}

Data Access with Spring Data JPA

Entity Definition

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(name = "created_at", updatable = false)
    @CreatedDate
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    // Relationships
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Post> posts = new ArrayList<>();
    
    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
    
    // getters and setters
}

Repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Method name query derivation
    List<User> findByName(String name);
    List<User> findByEmailContaining(String email);
    List<User> findByAgeGreaterThan(int age);
    
    // @Query with JPQL
    @Query("SELECT u FROM User u WHERE u.email = ?1")
    Optional<User> findByEmail(String email);
    
    // Native query
    @Query(value = "SELECT * FROM users WHERE name LIKE %?1%", nativeQuery = true)
    List<User> searchByName(String name);
    
    // Modifying query
    @Modifying
    @Transactional
    @Query("UPDATE User u SET u.name = ?2 WHERE u.id = ?1")
    int updateName(Long id, String name);
    
    // Pagination and sorting
    Page<User> findByAgeGreaterThan(int age, Pageable pageable);
}

Service Layer

@Service
@Transactional
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }
    
    @Transactional(readOnly = true)
    public Optional<User> findById(Long id) {
        return userRepository.findById(id);
    }
    
    public User save(User user) {
        return userRepository.save(user);
    }
    
    public Optional<User> update(Long id, User userDetails) {
        return userRepository.findById(id)
            .map(user -> {
                user.setName(userDetails.getName());
                user.setEmail(userDetails.getEmail());
                return userRepository.save(user);
            });
    }
    
    public boolean delete(Long id) {
        return userRepository.findById(id)
            .map(user -> {
                userRepository.delete(user);
                return true;
            })
            .orElse(false);
    }
    
    @Transactional(readOnly = true)
    public Page<User> findPaginated(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("name").ascending());
        return userRepository.findAll(pageable);
    }
}

Production Features

Actuator Endpoints

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# Enable all endpoints
management.endpoints.web.exposure.include=*

# Or specific endpoints
management.endpoints.web.exposure.include=health,info,metrics

# Customize endpoint paths
management.endpoints.web.base-path=/actuator

# Show detailed health info
management.endpoint.health.show-details=always
Available Endpoints:
  • /actuator/health - Application health
  • /actuator/info - Application info
  • /actuator/metrics - Application metrics
  • /actuator/env - Environment properties
  • /actuator/loggers - Logger configuration

Custom Health Indicator

@Component
public class CustomHealthIndicator implements HealthIndicator {
    
    @Override
    public Health health() {
        try {
            // Check your custom health criteria
            boolean serviceUp = checkExternalService();
            
            if (serviceUp) {
                return Health.up()
                    .withDetail("service", "Available")
                    .build();
            } else {
                return Health.down()
                    .withDetail("service", "Unavailable")
                    .build();
            }
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
    
    private boolean checkExternalService() {
        // Implementation
        return true;
    }
}

Application Metrics

@Service
public class MetricsService {
    
    private final MeterRegistry meterRegistry;
    private final Counter userCreationCounter;
    
    public MetricsService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.userCreationCounter = Counter.builder("users.created")
            .description("Number of users created")
            .register(meterRegistry);
    }
    
    public void recordUserCreation() {
        userCreationCounter.increment();
    }
    
    @Timed(value = "user.fetch.time", description = "Time taken to fetch user")
    public User fetchUser(Long id) {
        // Implementation
        return null;
    }
}

Testing

Unit Tests

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testFindAll() {
        List<User> users = Arrays.asList(new User(), new User());
        when(userRepository.findAll()).thenReturn(users);
        
        List<User> result = userService.findAll();
        
        assertEquals(2, result.size());
        verify(userRepository).findAll();
    }
}

Integration Tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void testCreateUser() throws Exception {
        User user = new User();
        user.setName("John Doe");
        user.setEmail("[email protected]");
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John Doe"))
            .andExpect(jsonPath("$.email").value("[email protected]"));
    }
}

Best Practices

1

Use Starters Wisely

  • Include only necessary starters
  • Avoid version conflicts
  • Let Spring Boot manage versions
2

Externalize Configuration

  • Use profiles for different environments
  • Store sensitive data in environment variables
  • Use Spring Cloud Config for distributed config
3

Follow REST Conventions

  • Use proper HTTP methods
  • Return appropriate status codes
  • Version your APIs
4

Implement Proper Error Handling

  • Use @ControllerAdvice
  • Return consistent error responses
  • Log errors appropriately
5

Monitor and Measure

  • Enable Actuator in production
  • Set up custom metrics
  • Use distributed tracing

Spring Framework

Core Spring concepts

Java Fundamentals

Java basics

Concurrency

Multithreading in Java

Build docs developers (and LLMs) love