Spring Boot makes it easy to create stand-alone, production-grade Spring-based applications that you can "just run".
1. Spring Boot Project Structure
Typical Project Structure
my-spring-boot-app/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/app/ │ │ │ ├── MySpringBootApp.java │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── repository/ │ │ │ └── model/ │ │ └── resources/ │ │ ├── application.properties │ │ ├── static/ │ │ └── templates/ │ └── test/ │ └── java/ ├── pom.xml (Maven) └── build.gradle (Gradle)
2. Creating a Spring Boot Application
Main Application Class
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class, args);
}
}
Using SpringApplicationBuilder for Advanced Configuration
package com.example.demo;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class MySpringBootApp {
public static void main(String[] args) {
new SpringApplicationBuilder(MySpringBootApp.class)
.bannerMode(Banner.Mode.OFF) // Turn off banner
.logStartupInfo(false) // Disable startup info logging
.run(args);
}
}
3. Spring Boot Starters
Common Starters in pom.xml (Maven)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>my-spring-boot-app</artifactId> <version>1.0.0</version> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- Web Starter for REST APIs --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Data JPA Starter for Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Security Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Test Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- DevTools for development --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- Database Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
4. Spring Boot 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=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA/Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
# Logging Configuration
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.web=INFO
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Custom Application Properties
app.name=My Spring Boot App
app.version=1.0.0
app.description=Demo Spring Boot Application
application.yml (Alternative)
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
logging:
level:
com.example.demo: DEBUG
org.springframework.web: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
app:
name: "My Spring Boot App"
version: "1.0.0"
description: "Demo Spring Boot Application"
Configuration Properties Class
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name;
private String version;
private String description;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
@Override
public String toString() {
return String.format("AppConfig{name='%s', version='%s', description='%s'}",
name, version, description);
}
}
5. Spring Boot Controllers
Basic REST Controller
package com.example.demo.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public String getAllUsers() {
return "Getting all users";
}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "Getting user with id: " + id;
}
@PostMapping
public String createUser(@RequestBody User user) {
return "Creating user: " + user;
}
@PutMapping("/{id}")
public String updateUser(@PathVariable Long id, @RequestBody User user) {
return "Updating user with id: " + id + " with data: " + user;
}
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
return "Deleting user with id: " + id;
}
}
// DTO (Data Transfer Object)
class User {
private Long id;
private String name;
private String email;
// Constructors
public User() {}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return String.format("User{id=%d, name='%s', email='%s'}", id, name, email);
}
}
Advanced Controller with ResponseEntity
package com.example.demo.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private List<Product> products = new ArrayList<>();
private Long nextId = 1L;
public ProductController() {
// Initialize with some sample data
products.add(new Product(nextId++, "Laptop", 999.99));
products.add(new Product(nextId++, "Mouse", 29.99));
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<?> getProductById(@PathVariable Long id) {
Optional<Product> product = products.stream()
.filter(p -> p.getId().equals(id))
.findFirst();
if (product.isPresent()) {
return ResponseEntity.ok(product.get());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("Product not found with id: " + id));
}
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
product.setId(nextId++);
products.add(product);
return ResponseEntity.status(HttpStatus.CREATED).body(product);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateProduct(@PathVariable Long id, @RequestBody Product updatedProduct) {
for (int i = 0; i < products.size(); i++) {
if (products.get(i).getId().equals(id)) {
updatedProduct.setId(id);
products.set(i, updatedProduct);
return ResponseEntity.ok(updatedProduct);
}
}
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("Product not found with id: " + id));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
boolean removed = products.removeIf(product -> product.getId().equals(id));
if (removed) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
}
// Query parameters example
@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice) {
List<Product> result = products;
if (name != null) {
result = result.stream()
.filter(p -> p.getName().toLowerCase().contains(name.toLowerCase()))
.toList();
}
if (minPrice != null) {
result = result.stream()
.filter(p -> p.getPrice() >= minPrice)
.toList();
}
if (maxPrice != null) {
result = result.stream()
.filter(p -> p.getPrice() <= maxPrice)
.toList();
}
return ResponseEntity.ok(result);
}
}
class Product {
private Long id;
private String name;
private Double price;
// Constructors, getters, setters
public Product() {}
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
@Override
public String toString() {
return String.format("Product{id=%d, name='%s', price=%.2f}", id, name, price);
}
}
class ErrorResponse {
private String message;
private Date timestamp;
public ErrorResponse(String message) {
this.message = message;
this.timestamp = new Date();
}
// Getters and setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Date getTimestamp() { return timestamp; }
public void setTimestamp(Date timestamp) { this.timestamp = timestamp; }
}
6. Spring Boot Services
Service Layer with Business Logic
package com.example.demo.service;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class UserService {
private Map<Long, User> users = new HashMap<>();
private Long nextId = 1L;
public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}
public Optional<User> getUserById(Long id) {
return Optional.ofNullable(users.get(id));
}
public User createUser(User user) {
user.setId(nextId++);
users.put(user.getId(), user);
return user;
}
public Optional<User> updateUser(Long id, User updatedUser) {
if (users.containsKey(id)) {
updatedUser.setId(id);
users.put(id, updatedUser);
return Optional.of(updatedUser);
}
return Optional.empty();
}
public boolean deleteUser(Long id) {
return users.remove(id) != null;
}
public List<User> searchUsers(String name, String email) {
return users.values().stream()
.filter(user ->
(name == null || user.getName().toLowerCase().contains(name.toLowerCase())) &&
(email == null || user.getEmail().toLowerCase().contains(email.toLowerCase())))
.toList();
}
}
// User entity
class User {
private Long id;
private String name;
private String email;
private Date createdAt;
public User() {
this.createdAt = new Date();
}
public User(String name, String email) {
this();
this.name = name;
this.email = email;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Date getCreatedAt() { return createdAt; }
public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
@Override
public String toString() {
return String.format("User{id=%d, name='%s', email='%s', createdAt=%s}",
id, name, email, createdAt);
}
}
7. Spring Data JPA Integration
Entity and Repository
package com.example.demo.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name", nullable = false, length = 50)
private String firstName;
@Column(name = "last_name", nullable = false, length = 50)
private String lastName;
@Column(unique = true, nullable = false)
private String email;
private Double salary;
@Column(name = "department")
private String department;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Pre-persist and pre-update callbacks
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public Employee() {}
public Employee(String firstName, String lastName, String email, Double salary, String department) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.salary = salary;
this.department = department;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Double getSalary() { return salary; }
public void setSalary(Double salary) { this.salary = salary; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
@Override
public String toString() {
return String.format("Employee{id=%d, firstName='%s', lastName='%s', email='%s', department='%s'}",
id, firstName, lastName, email, department);
}
}
Spring Data JPA Repository
package com.example.demo.repository;
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 com.example.demo.entity.Employee;
import java.util.List;
import java.util.Optional;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
// Custom query methods
Optional<Employee> findByEmail(String email);
List<Employee> findByDepartment(String department);
List<Employee> findBySalaryGreaterThan(Double salary);
List<Employee> findByDepartmentAndSalaryGreaterThan(String department, Double salary);
// Custom query with @Query
@Query("SELECT e FROM Employee e WHERE e.firstName LIKE %:name% OR e.lastName LIKE %:name%")
List<Employee> findByNameContaining(@Param("name") String name);
@Query("SELECT e FROM Employee e WHERE e.salary BETWEEN :minSalary AND :maxSalary")
List<Employee> findBySalaryRange(@Param("minSalary") Double minSalary,
@Param("maxSalary") Double maxSalary);
@Query("SELECT e.department, AVG(e.salary) FROM Employee e GROUP BY e.department")
List<Object[]> findAverageSalaryByDepartment();
// Native SQL query
@Query(value = "SELECT * FROM employees e WHERE e.department = :dept ORDER BY e.salary DESC LIMIT 5",
nativeQuery = true)
List<Employee> findTop5ByDepartment(@Param("dept") String department);
}
Service using Repository
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.entity.Employee;
import com.example.demo.repository.EmployeeRepository;
import java.util.List;
import java.util.Optional;
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
public List<Employee> getAllEmployees() {
return employeeRepository.findAll();
}
public Optional<Employee> getEmployeeById(Long id) {
return employeeRepository.findById(id);
}
public Optional<Employee> getEmployeeByEmail(String email) {
return employeeRepository.findByEmail(email);
}
public Employee createEmployee(Employee employee) {
return employeeRepository.save(employee);
}
public Optional<Employee> updateEmployee(Long id, Employee employeeDetails) {
return employeeRepository.findById(id)
.map(employee -> {
employee.setFirstName(employeeDetails.getFirstName());
employee.setLastName(employeeDetails.getLastName());
employee.setEmail(employeeDetails.getEmail());
employee.setSalary(employeeDetails.getSalary());
employee.setDepartment(employeeDetails.getDepartment());
return employeeRepository.save(employee);
});
}
public boolean deleteEmployee(Long id) {
if (employeeRepository.existsById(id)) {
employeeRepository.deleteById(id);
return true;
}
return false;
}
public List<Employee> getEmployeesByDepartment(String department) {
return employeeRepository.findByDepartment(department);
}
public List<Employee> getHighSalaryEmployees(Double minSalary) {
return employeeRepository.findBySalaryGreaterThan(minSalary);
}
public List<Employee> searchEmployeesByName(String name) {
return employeeRepository.findByNameContaining(name);
}
public List<Object[]> getAverageSalaryByDepartment() {
return employeeRepository.findAverageSalaryByDepartment();
}
}
8. Exception Handling
Global Exception Handler
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.util.Date;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorDetails> handleResourceNotFoundException(
ResourceNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(
new Date(),
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDetails> handleGlobalException(
Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(
new Date(),
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Custom Exception
class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// Error Details DTO
class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
// Getters and setters
public Date getTimestamp() { return timestamp; }
public void setTimestamp(Date timestamp) { this.timestamp = timestamp; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getDetails() { return details; }
public void setDetails(String details) { this.details = details; }
}
9. Testing in Spring Boot
Unit Tests and Integration Tests
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.example.demo.service.EmployeeService;
import com.example.demo.entity.Employee;
import com.example.demo.repository.EmployeeRepository;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SpringBootTest
class MySpringBootAppTests {
@Test
void contextLoads() {
// Test that the application context loads successfully
}
}
// Service Unit Test
package com.example.demo.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.example.demo.entity.Employee;
import com.example.demo.repository.EmployeeRepository;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class EmployeeServiceTest {
@Mock
private EmployeeRepository employeeRepository;
@InjectMocks
private EmployeeService employeeService;
@Test
void testGetAllEmployees() {
// Given
Employee emp1 = new Employee("John", "Doe", "[email protected]", 50000.0, "IT");
Employee emp2 = new Employee("Jane", "Smith", "[email protected]", 60000.0, "HR");
List<Employee> employees = Arrays.asList(emp1, emp2);
when(employeeRepository.findAll()).thenReturn(employees);
// When
List<Employee> result = employeeService.getAllEmployees();
// Then
assertEquals(2, result.size());
verify(employeeRepository, times(1)).findAll();
}
@Test
void testGetEmployeeById() {
// Given
Long employeeId = 1L;
Employee employee = new Employee("John", "Doe", "[email protected]", 50000.0, "IT");
employee.setId(employeeId);
when(employeeRepository.findById(employeeId)).thenReturn(Optional.of(employee));
// When
Optional<Employee> result = employeeService.getEmployeeById(employeeId);
// Then
assertTrue(result.isPresent());
assertEquals("John", result.get().getFirstName());
verify(employeeRepository, times(1)).findById(employeeId);
}
@Test
void testCreateEmployee() {
// Given
Employee employee = new Employee("John", "Doe", "[email protected]", 50000.0, "IT");
Employee savedEmployee = new Employee("John", "Doe", "[email protected]", 50000.0, "IT");
savedEmployee.setId(1L);
when(employeeRepository.save(any(Employee.class))).thenReturn(savedEmployee);
// When
Employee result = employeeService.createEmployee(employee);
// Then
assertNotNull(result.getId());
assertEquals("John", result.getFirstName());
verify(employeeRepository, times(1)).save(employee);
}
}
10. Spring Boot Actuator
Adding Actuator Dependencies and Configuration
<!-- In pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
application.properties for Actuator
# Actuator Configuration management.endpoints.web.exposure.include=health,info,metrics,beans,env management.endpoint.health.show-details=always management.endpoint.health.show-components=always # Custom Info endpoint info.app.name=My Spring Boot App info.app.version=1.0.0 info.app.description=Demo Application with Actuator
Custom Health Check
package com.example.demo.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Custom health check logic
boolean isHealthy = checkSystemHealth();
if (isHealthy) {
return Health.up()
.withDetail("database", "Connected")
.withDetail("diskSpace", "Sufficient")
.build();
} else {
return Health.down()
.withDetail("database", "Disconnected")
.withDetail("error", "System not healthy")
.build();
}
}
private boolean checkSystemHealth() {
// Implement actual health check logic
return true; // Simplified for example
}
}
Key Spring Boot Features Summary
- Auto-configuration - Automatic setup based on dependencies
- Starter Dependencies - Pre-configured dependency sets
- Embedded Servers - Tomcat, Jetty, or Undertow
- Production-ready Features - Actuator for monitoring
- Spring Boot CLI - Command-line interface
- Externalized Configuration - Properties/YAML files
- Profiles - Environment-specific configurations
Spring Boot dramatically reduces the configuration overhead and lets you focus on business logic while providing enterprise-ready features out of the box.