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.