Quick Start
Want to quickly understand Spring Boot’s core concepts? This quick start guide provides a fast-paced tour through the fundamental concepts that make Spring Boot work. By the end, you’ll understand the architecture, patterns, and tools needed to build production-ready applications.
This tutorial provides 5-30% coverage - enough to understand Spring Boot’s architecture and start building. For deeper learning, continue to Beginner Tutorial (0-60% coverage) or explore By Example for code-first learning.
Prerequisites
Before starting this quick start, you should have:
- Spring Boot installed and working (complete Initial Setup first)
- Basic Java or Kotlin knowledge (classes, methods, annotations)
- Text editor or IDE (IntelliJ IDEA recommended)
- Terminal access
- Basic HTTP understanding (GET, POST, status codes)
Learning Path
This quick start follows a structured learning path:
graph TD
A[Project Setup] --> B[Controllers & REST]
B --> C[Dependency Injection]
C --> D[Services & Business Logic]
D --> E[Data Models]
E --> F[Repository Pattern]
F --> G[Database Integration]
G --> H[Exception Handling]
H --> I[Configuration]
I --> J[Testing]
J --> K[Production Features]
K --> L[Build & Deploy]
style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style B fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style C fill:#fff3e0,stroke:#e65100,stroke-width:2px
style D fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style E fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style F fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style G fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style H fill:#fff3e0,stroke:#e65100,stroke-width:2px
style I fill:#fff3e0,stroke:#e65100,stroke-width:2px
style J fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
style K fill:#e0f2f1,stroke:#004d40,stroke-width:2px
style L fill:#e0f2f1,stroke:#004d40,stroke-width:2px
Color Key:
- Blue - Web Layer (Controllers, REST)
- Orange - Core Concepts (DI, Configuration, Error Handling)
- Green - Data Layer (Models, Repositories, Database)
- Purple - Testing
- Teal - Production & Deployment
What You’ll Build
Throughout this tutorial, you’ll build a Task Management API that demonstrates Spring Boot’s core features:
- REST endpoints for CRUD operations
- Service layer for business logic
- Database integration with PostgreSQL
- Exception handling and validation
- Configuration management
- Unit and integration tests
Concept 1: Spring Boot Application Structure
Spring Boot applications follow layered architecture with clear separation of concerns.
Three-Layer Architecture
graph TD
A[Controller Layer] -->|calls| B[Service Layer]
B -->|calls| C[Repository Layer]
C -->|queries| D[(Database)]
style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style B fill:#fff3e0,stroke:#e65100,stroke-width:2px
style C fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style D fill:#fce4ec,stroke:#880e4f,stroke-width:2px
Layer responsibilities:
- Controller - Handles HTTP requests, validates input, returns responses
- Service - Contains business logic, orchestrates operations
- Repository - Manages database operations, executes queries
Project Structure Example
src/main/java/com/example/taskmanager/
├── TaskManagerApplication.java # Main entry point
├── controller/
│ └── TaskController.java # REST endpoints
├── service/
│ └── TaskService.java # Business logic
├── repository/
│ └── TaskRepository.java # Database access
├── model/
│ └── Task.java # Entity/domain model
├── dto/
│ ├── TaskRequest.java # Request data transfer object
│ └── TaskResponse.java # Response data transfer object
├── exception/
│ ├── TaskNotFoundException.java
│ └── GlobalExceptionHandler.java
└── config/
└── DatabaseConfig.java # Configuration classesKey principles:
- Each layer has single responsibility
- Layers depend only on layers below them
- Controllers never access repositories directly
- Business logic stays in service layer
Concept 2: REST Controllers
Controllers handle HTTP requests and return responses.
Basic Controller
Java:
package com.example.taskmanager.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // => Marks class as REST controller
@RequestMapping("/api/tasks") // => Base path for all endpoints
public class TaskController {
@GetMapping // => Maps GET /api/tasks
public String getAllTasks() { // => Method handles GET request
return "Task list"; // => Returns plain text response
} // => Spring serializes return value automatically
}Kotlin:
package com.example.taskmanager.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController // => Marks class as REST controller
@RequestMapping("/api/tasks") // => Base path for all endpoints
class TaskController {
@GetMapping // => Maps GET /api/tasks
fun getAllTasks(): String { // => Method handles GET request
return "Task list" // => Returns plain text response
} // => Spring serializes return value automatically
}Test the endpoint:
curl http://localhost:8080/api/tasksKey annotations:
@RestController- Combines@Controllerand@ResponseBody(auto-serializes responses to JSON)@RequestMapping- Base path for all methods in controller@GetMapping- Maps HTTP GET requests to method
Handling Path Variables
Java:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@GetMapping("/{id}") // => Path template with variable
public String getTask(@PathVariable Long id) {
// => @PathVariable extracts id from path
return "Task #" + id; // => id captured from URL path
} // => Returns: "Task #123"Kotlin:
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
@GetMapping("/{id}") // => Path template with variable
fun getTask(@PathVariable id: Long): String {
// => @PathVariable extracts id from path
return "Task #$id" // => id captured from URL path
} // => Returns: "Task #123"Test:
curl http://localhost:8080/api/tasks/123Handling Query Parameters
Java:
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/search") // => Endpoint: GET /api/tasks/search
public String searchTasks(
@RequestParam(required = false) String status,
// => Extracts ?status=completed
@RequestParam(defaultValue = "10") int limit
// => Default value if not provided
) {
return "Search: status=" + status + ", limit=" + limit;
// => Returns formatted string
}Kotlin:
import org.springframework.web.bind.annotation.RequestParam
@GetMapping("/search") // => Endpoint: GET /api/tasks/search
fun searchTasks(
@RequestParam(required = false) status: String?,
// => Extracts ?status=completed (nullable)
@RequestParam(defaultValue = "10") limit: Int
// => Default value if not provided
): String {
return "Search: status=$status, limit=$limit"
// => Returns formatted string
}Test:
curl "http://localhost:8080/api/tasks/search?status=completed&limit=5"Concept 3: Dependency Injection (DI)
Spring Boot’s core feature - automatic object creation and wiring.
Why Dependency Injection?
Without DI (manual object creation):
Java:
public class TaskController {
private TaskService service = new TaskService();
// => Manually creates service instance
// => Controller tightly coupled to service
// => Hard to test (can't mock service)
}With DI (Spring manages objects):
Java:
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class TaskController {
private final TaskService service;
// => Service instance provided by Spring
@Autowired // => Spring injects dependency
public TaskController(TaskService service) {
// => Constructor injection (recommended)
this.service = service; // => Stores injected service
} // => Service created by Spring, not manually
}Kotlin (simpler with primary constructor):
@RestController
class TaskController(
private val service: TaskService // => Spring injects automatically
) // => No @Autowired needed in Kotlin
// => Service provided by SpringBenefits:
- Loose coupling (controller doesn’t create service)
- Easy testing (inject mock service)
- Single Responsibility Principle (Spring manages object lifecycle)
- Automatic configuration
Component Scanning
Spring automatically discovers and creates objects marked with stereotypes:
@RestController- Web controllers@Service- Service layer@Repository- Data access layer@Component- Generic managed component
Java Service Example:
package com.example.taskmanager.service;
import org.springframework.stereotype.Service;
@Service // => Marks class as Spring-managed service
public class TaskService { // => Spring creates instance automatically
public String findAll() { // => Business logic method
return "All tasks from service";
// => Returns data (actual implementation would use repository)
}
}Kotlin Service Example:
package com.example.taskmanager.service
import org.springframework.stereotype.Service
@Service // => Marks class as Spring-managed service
class TaskService { // => Spring creates instance automatically
fun findAll(): String { // => Business logic method
return "All tasks from service"
// => Returns data (actual implementation would use repository)
}
}Use in Controller:
Java:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService service;
@Autowired
public TaskController(TaskService service) {
// => Spring injects TaskService instance
this.service = service;
}
@GetMapping
public String getAllTasks() {
return service.findAll(); // => Calls injected service
}
}Kotlin:
@RestController
@RequestMapping("/api/tasks")
class TaskController(
private val service: TaskService // => Spring injects TaskService instance
) {
@GetMapping
fun getAllTasks(): String {
return service.findAll() // => Calls injected service
}
}Concept 4: Data Transfer Objects (DTOs)
DTOs separate external API representation from internal domain models.
Why DTOs?
Without DTOs (exposing entity directly):
@Entity // => Database entity
public class Task {
private Long id;
private String password; // => Sensitive field exposed!
private String internalNotes; // => Internal field exposed!
// Getters/setters
}
@GetMapping
public Task getTask() {
return taskRepository.findById(1L);
// => Returns entity with ALL fields
// => password and internalNotes visible in JSON!
}With DTOs (controlled exposure):
Java:
// DTO for API responses
public class TaskResponse {
private Long id;
private String title;
private String status;
// Only public fields included
public TaskResponse(Long id, String title, String status) {
this.id = id; // => Explicit field selection
this.title = title;
this.status = status;
}
// Getters only (immutable response)
}Kotlin (data class):
// DTO for API responses
data class TaskResponse( // => Data class (automatic equals/hashCode/toString)
val id: Long, // => Immutable fields
val title: String,
val status: String
) // => Only public fields includedRequest and Response DTOs
Java Request DTO:
public class TaskRequest {
private String title; // => Field from request body
private String description;
private String status;
// Validation annotations
@NotBlank(message = "Title required")
public String getTitle() {
return title; // => Getter with validation
}
// Setters for deserialization
}Kotlin Request DTO:
data class TaskRequest(
@field:NotBlank(message = "Title required")
// => Validation on field
val title: String, // => Field from request body
val description: String?, // => Nullable field (optional)
val status: String
)Using DTOs in Controller:
Java:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping // => Maps POST /api/tasks
public TaskResponse createTask(@RequestBody TaskRequest request) {
// => @RequestBody deserializes JSON to TaskRequest
Task task = new Task(request.getTitle(), request.getDescription());
// => Create entity from DTO
Task saved = service.save(task); // => Save to database
return new TaskResponse(saved.getId(), saved.getTitle(), saved.getStatus());
// => Convert entity to response DTO
}Kotlin:
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
@PostMapping // => Maps POST /api/tasks
fun createTask(@RequestBody request: TaskRequest): TaskResponse {
// => @RequestBody deserializes JSON to TaskRequest
val task = Task(request.title, request.description)
// => Create entity from DTO
val saved = service.save(task) // => Save to database
return TaskResponse(saved.id, saved.title, saved.status)
// => Convert entity to response DTO
}Test with curl:
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Write docs","description":"Spring Boot tutorial","status":"pending"}'Concept 5: JPA Entities and Database Models
JPA (Java Persistence API) maps Java/Kotlin classes to database tables.
Basic Entity
Java:
package com.example.taskmanager.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity // => Marks class as database entity
@Table(name = "tasks") // => Maps to "tasks" table
public class Task {
@Id // => Primary key field
@GeneratedValue(strategy = GenerationType.IDENTITY)
// => Auto-increment ID
private Long id;
@Column(nullable = false, length = 200)
// => NOT NULL constraint, max length 200
private String title;
@Column(columnDefinition = "TEXT")
// => TEXT type for long content
private String description;
@Column(nullable = false)
private String status; // => NOT NULL status field
@Column(name = "created_at", updatable = false)
// => Maps to created_at column, read-only
private LocalDateTime createdAt;
@PrePersist // => Called before INSERT
protected void onCreate() {
createdAt = LocalDateTime.now();
// => Sets timestamp on creation
}
// Constructors, getters, setters
protected Task() {} // => JPA requires no-arg constructor
public Task(String title, String description) {
this.title = title;
this.description = description;
this.status = "pending"; // => Default status
}
}Kotlin (more concise):
package com.example.taskmanager.model
import jakarta.persistence.*
import java.time.LocalDateTime
@Entity // => Marks class as database entity
@Table(name = "tasks") // => Maps to "tasks" table
data class Task(
@Id // => Primary key field
@GeneratedValue(strategy = GenerationType.IDENTITY)
// => Auto-increment ID
val id: Long = 0, // => Default value for new entities
@Column(nullable = false, length = 200)
// => NOT NULL constraint, max length 200
var title: String,
@Column(columnDefinition = "TEXT")
// => TEXT type for long content
var description: String? = null, // => Nullable field
@Column(nullable = false)
var status: String = "pending", // => Default status
@Column(name = "created_at", updatable = false)
// => Maps to created_at column, read-only
val createdAt: LocalDateTime = LocalDateTime.now()
// => Initialized on creation
)Generated SQL (PostgreSQL):
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY, -- Auto-increment ID
title VARCHAR(200) NOT NULL, -- NOT NULL, max 200 chars
description TEXT, -- TEXT type, nullable
status VARCHAR(255) NOT NULL, -- NOT NULL status
created_at TIMESTAMP NOT NULL -- Timestamp field
);Common JPA Annotations
@Entity- Marks class as database entity@Table(name = "...")- Specifies table name@Id- Primary key field@GeneratedValue- Auto-generated value strategy@Column- Column properties (nullable, length, name)@PrePersist- Lifecycle callback before INSERT@PreUpdate- Lifecycle callback before UPDATE
Concept 6: Spring Data JPA Repositories
Repositories provide database operations without writing SQL.
Repository Interface
Java:
package com.example.taskmanager.repository;
import com.example.taskmanager.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository // => Marks as repository component
public interface TaskRepository extends JpaRepository<Task, Long> {
// => JpaRepository<Entity, ID type>
// => Provides CRUD methods automatically
// Custom query methods (derived from method name)
List<Task> findByStatus(String status);
// => SELECT * FROM tasks WHERE status = ?
// => Spring generates query automatically
List<Task> findByTitleContaining(String keyword);
// => SELECT * FROM tasks WHERE title LIKE %keyword%
// => LIKE query generated automatically
}Kotlin:
package com.example.taskmanager.repository
import com.example.taskmanager.model.Task
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository // => Marks as repository component
interface TaskRepository : JpaRepository<Task, Long> {
// => JpaRepository<Entity, ID type>
// => Provides CRUD methods automatically
// Custom query methods (derived from method name)
fun findByStatus(status: String): List<Task>
// => SELECT * FROM tasks WHERE status = ?
// => Spring generates query automatically
fun findByTitleContaining(keyword: String): List<Task>
// => SELECT * FROM tasks WHERE title LIKE %keyword%
// => LIKE query generated automatically
}Built-in Repository Methods
No implementation needed - Spring provides:
// CRUD operations (inherited from JpaRepository)
task = repository.save(task); // => INSERT or UPDATE
task = repository.findById(1L); // => SELECT by ID
List<Task> all = repository.findAll(); // => SELECT all
repository.deleteById(1L); // => DELETE by ID
boolean exists = repository.existsById(1L);
// => Check existence
long count = repository.count(); // => COUNT queryUsing Repository in Service
Java:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository repository;
@Autowired // => Inject repository
public TaskService(TaskRepository repository) {
this.repository = repository;
}
public List<Task> findAll() {
return repository.findAll(); // => SELECT * FROM tasks
}
public Task save(Task task) {
return repository.save(task); // => INSERT INTO tasks
}
public List<Task> findByStatus(String status) {
return repository.findByStatus(status);
// => Custom query method
}
}Kotlin:
import org.springframework.stereotype.Service
@Service
class TaskService(
private val repository: TaskRepository
// => Inject repository (no @Autowired needed)
) {
fun findAll(): List<Task> {
return repository.findAll() // => SELECT * FROM tasks
}
fun save(task: Task): Task {
return repository.save(task) // => INSERT INTO tasks
}
fun findByStatus(status: String): List<Task> {
return repository.findByStatus(status)
// => Custom query method
}
}Concept 7: Database Configuration
Configure database connection in application.properties.
PostgreSQL Configuration
File: src/main/resources/application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/taskdb
# => PostgreSQL connection URL
spring.datasource.username=dbuser # => Database username
spring.datasource.password=dbpass # => Database password
spring.datasource.driver-class-name=org.postgresql.Driver
# => JDBC driver class
spring.jpa.hibernate.ddl-auto=update # => Auto-update schema (create tables)
# => Options: none, validate, update, create, create-drop
spring.jpa.show-sql=true # => Log SQL queries to console
spring.jpa.properties.hibernate.format_sql=true
# => Format SQL for readability
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# => PostgreSQL-specific SQL dialect
spring.datasource.hikari.maximum-pool-size=10
# => Max 10 database connectionsAdd PostgreSQL Dependency
Maven (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>Gradle (build.gradle.kts):
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")Environment-Specific Configuration
Use profiles for different environments:
File: application-dev.properties (development)
spring.datasource.url=jdbc:postgresql://localhost:5432/taskdb_dev
spring.jpa.hibernate.ddl-auto=create-drop
# => Drop and recreate schema on restartFile: application-prod.properties (production)
spring.datasource.url=jdbc:postgresql://prod-db:5432/taskdb
spring.jpa.hibernate.ddl-auto=validate
# => Only validate schema, don't modifyActivate profile:
./mvnw spring-boot:run -Dspring-boot.run.profiles=devConcept 8: Exception Handling
Handle errors gracefully with custom exceptions and global exception handler.
Custom Exception
Java:
package com.example.taskmanager.exception;
public class TaskNotFoundException extends RuntimeException {
// => Custom exception for missing tasks
public TaskNotFoundException(Long id) {
super("Task not found with id: " + id);
// => Error message with task ID
}
}Kotlin:
package com.example.taskmanager.exception
class TaskNotFoundException(id: Long) : RuntimeException("Task not found with id: $id")
// => Custom exception for missing tasksGlobal Exception Handler
Java:
package com.example.taskmanager.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;
@RestControllerAdvice // => Global exception handler for all controllers
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
// => Handles TaskNotFoundException
public ResponseEntity<ErrorResponse> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage() // => Extract error message
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
// => Returns 404 with error details
}
}
class ErrorResponse {
private int status;
private String message;
public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
// Getters
}Kotlin:
package com.example.taskmanager.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
@RestControllerAdvice // => Global exception handler for all controllers
class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException::class)
// => Handles TaskNotFoundException
fun handleTaskNotFound(ex: TaskNotFoundException): ResponseEntity<ErrorResponse> {
val error = ErrorResponse(
status = HttpStatus.NOT_FOUND.value(),
message = ex.message ?: "Task not found"
// => Extract error message
)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error)
// => Returns 404 with error details
}
}
data class ErrorResponse(
val status: Int,
val message: String
)Using Custom Exception
Java:
@Service
public class TaskService {
private final TaskRepository repository;
public Task findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
// => Throws exception if not found
// => Exception caught by GlobalExceptionHandler
}
}Kotlin:
@Service
class TaskService(private val repository: TaskRepository) {
fun findById(id: Long): Task {
return repository.findById(id)
.orElseThrow { TaskNotFoundException(id) }
// => Throws exception if not found
// => Exception caught by GlobalExceptionHandler
}
}Error response:
curl http://localhost:8080/api/tasks/999Concept 9: Complete CRUD Operations
Combine all concepts into full CRUD API.
Service Layer
Java:
@Service
public class TaskService {
private final TaskRepository repository;
@Autowired
public TaskService(TaskRepository repository) {
this.repository = repository;
}
public List<Task> findAll() {
return repository.findAll(); // => SELECT * FROM tasks
}
public Task findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
}
public Task create(Task task) {
task.setStatus("pending"); // => Set default status
return repository.save(task); // => INSERT
}
public Task update(Long id, Task updates) {
Task existing = findById(id); // => Load existing task
existing.setTitle(updates.getTitle());
existing.setDescription(updates.getDescription());
existing.setStatus(updates.getStatus());
return repository.save(existing);
// => UPDATE
}
public void delete(Long id) {
if (!repository.existsById(id)) {
throw new TaskNotFoundException(id);
}
repository.deleteById(id); // => DELETE
}
}Kotlin:
@Service
class TaskService(private val repository: TaskRepository) {
fun findAll(): List<Task> {
return repository.findAll() // => SELECT * FROM tasks
}
fun findById(id: Long): Task {
return repository.findById(id)
.orElseThrow { TaskNotFoundException(id) }
}
fun create(task: Task): Task {
task.status = "pending" // => Set default status
return repository.save(task) // => INSERT
}
fun update(id: Long, updates: Task): Task {
val existing = findById(id) // => Load existing task
existing.title = updates.title
existing.description = updates.description
existing.status = updates.status
return repository.save(existing)
// => UPDATE
}
fun delete(id: Long) {
if (!repository.existsById(id)) {
throw TaskNotFoundException(id)
}
repository.deleteById(id) // => DELETE
}
}Controller with Full CRUD
Java:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService service;
@Autowired
public TaskController(TaskService service) {
this.service = service;
}
@GetMapping // => GET /api/tasks
public List<TaskResponse> getAllTasks() {
return service.findAll().stream()
.map(task -> new TaskResponse(task.getId(), task.getTitle(), task.getStatus()))
.toList(); // => Convert entities to DTOs
}
@GetMapping("/{id}") // => GET /api/tasks/{id}
public TaskResponse getTask(@PathVariable Long id) {
Task task = service.findById(id);
return new TaskResponse(task.getId(), task.getTitle(), task.getStatus());
}
@PostMapping // => POST /api/tasks
@ResponseStatus(HttpStatus.CREATED)
// => Returns 201 Created
public TaskResponse createTask(@RequestBody TaskRequest request) {
Task task = new Task(request.getTitle(), request.getDescription());
Task saved = service.create(task);
return new TaskResponse(saved.getId(), saved.getTitle(), saved.getStatus());
}
@PutMapping("/{id}") // => PUT /api/tasks/{id}
public TaskResponse updateTask(@PathVariable Long id, @RequestBody TaskRequest request) {
Task updates = new Task(request.getTitle(), request.getDescription());
Task updated = service.update(id, updates);
return new TaskResponse(updated.getId(), updated.getTitle(), updated.getStatus());
}
@DeleteMapping("/{id}") // => DELETE /api/tasks/{id}
@ResponseStatus(HttpStatus.NO_CONTENT)
// => Returns 204 No Content
public void deleteTask(@PathVariable Long id) {
service.delete(id);
}
}Kotlin:
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/tasks")
class TaskController(private val service: TaskService) {
@GetMapping // => GET /api/tasks
fun getAllTasks(): List<TaskResponse> {
return service.findAll()
.map { TaskResponse(it.id, it.title, it.status) }
// => Convert entities to DTOs
}
@GetMapping("/{id}") // => GET /api/tasks/{id}
fun getTask(@PathVariable id: Long): TaskResponse {
val task = service.findById(id)
return TaskResponse(task.id, task.title, task.status)
}
@PostMapping // => POST /api/tasks
@ResponseStatus(HttpStatus.CREATED)
// => Returns 201 Created
fun createTask(@RequestBody request: TaskRequest): TaskResponse {
val task = Task(title = request.title, description = request.description)
val saved = service.create(task)
return TaskResponse(saved.id, saved.title, saved.status)
}
@PutMapping("/{id}") // => PUT /api/tasks/{id}
fun updateTask(@PathVariable id: Long, @RequestBody request: TaskRequest): TaskResponse {
val updates = Task(title = request.title, description = request.description)
val updated = service.update(id, updates)
return TaskResponse(updated.id, updated.title, updated.status)
}
@DeleteMapping("/{id}") // => DELETE /api/tasks/{id}
@ResponseStatus(HttpStatus.NO_CONTENT)
// => Returns 204 No Content
fun deleteTask(@PathVariable id: Long) {
service.delete(id)
}
}Test CRUD operations:
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Learn Spring Boot","description":"Complete tutorial"}'
curl http://localhost:8080/api/tasks
curl http://localhost:8080/api/tasks/1
curl -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title":"Learn Spring Boot - Updated","description":"Finished tutorial","status":"completed"}'
curl -X DELETE http://localhost:8080/api/tasks/1Concept 10: Testing Spring Boot Applications
Write unit and integration tests for controllers, services, and repositories.
Unit Testing Service Layer
Java with JUnit 5 and Mockito:
package com.example.taskmanager.service;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.repository.TaskRepository;
import com.example.taskmanager.exception.TaskNotFoundException;
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 java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) // => Enable Mockito for JUnit 5
class TaskServiceTest {
@Mock // => Mock repository dependency
private TaskRepository repository;
@InjectMocks // => Inject mocks into service
private TaskService service;
@Test
void findById_existingTask_returnsTask() {
// Arrange
Task task = new Task("Test", "Description");
when(repository.findById(1L)).thenReturn(Optional.of(task));
// => Mock returns task when ID=1
// Act
Task result = service.findById(1L);
// => Call service method
// Assert
assertNotNull(result); // => Verify result not null
assertEquals("Test", result.getTitle());
// => Verify correct task returned
verify(repository).findById(1L);
// => Verify repository called once
}
@Test
void findById_nonExistingTask_throwsException() {
// Arrange
when(repository.findById(999L)).thenReturn(Optional.empty());
// => Mock returns empty for ID=999
// Act & Assert
assertThrows(TaskNotFoundException.class, () -> service.findById(999L));
// => Verify exception thrown
}
}Kotlin with JUnit 5 and MockK:
package com.example.taskmanager.service
import com.example.taskmanager.model.Task
import com.example.taskmanager.repository.TaskRepository
import com.example.taskmanager.exception.TaskNotFoundException
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.Optional
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class TaskServiceTest {
private val repository: TaskRepository = mockk()
// => Mock repository dependency
private val service = TaskService(repository)
// => Inject mock into service
@Test
fun `findById with existing task returns task`() {
// Arrange
val task = Task(id = 1, title = "Test", description = "Description")
every { repository.findById(1L) } returns Optional.of(task)
// => Mock returns task when ID=1
// Act
val result = service.findById(1L)
// => Call service method
// Assert
assertNotNull(result) // => Verify result not null
assertEquals("Test", result.title)
// => Verify correct task returned
verify { repository.findById(1L) }
// => Verify repository called once
}
@Test
fun `findById with non-existing task throws exception`() {
// Arrange
every { repository.findById(999L) } returns Optional.empty()
// => Mock returns empty for ID=999
// Act & Assert
assertThrows<TaskNotFoundException> { service.findById(999L) }
// => Verify exception thrown
}
}Integration Testing Controller
Java with Spring Boot Test:
package com.example.taskmanager.controller;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.repository.TaskRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest // => Load full Spring context
@AutoConfigureMockMvc // => Configure MockMvc for HTTP testing
class TaskControllerIntegrationTest {
@Autowired
private MockMvc mockMvc; // => Mock HTTP client
@Autowired
private TaskRepository repository;
@BeforeEach
void setUp() {
repository.deleteAll(); // => Clean database before each test
}
@Test
void createTask_validRequest_returnsCreated() throws Exception {
String requestBody = """
{"title":"Test Task","description":"Test Description"}
""";
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
// => POST request with JSON body
.andExpect(status().isCreated())
// => Verify 201 Created
.andExpect(jsonPath("$.title").value("Test Task"))
// => Verify response JSON
.andExpect(jsonPath("$.status").value("pending"));
}
@Test
void getTask_existingId_returnsTask() throws Exception {
Task task = repository.save(new Task("Existing", "Description"));
// => Save task to database
mockMvc.perform(get("/api/tasks/" + task.getId()))
// => GET request
.andExpect(status().isOk())
// => Verify 200 OK
.andExpect(jsonPath("$.title").value("Existing"));
}
@Test
void getTask_nonExistingId_returnsNotFound() throws Exception {
mockMvc.perform(get("/api/tasks/999"))
.andExpect(status().isNotFound());
// => Verify 404 Not Found
}
}Kotlin with Spring Boot Test:
package com.example.taskmanager.controller
import com.example.taskmanager.model.Task
import com.example.taskmanager.repository.TaskRepository
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@SpringBootTest // => Load full Spring context
@AutoConfigureMockMvc // => Configure MockMvc for HTTP testing
class TaskControllerIntegrationTest {
@Autowired
private lateinit var mockMvc: MockMvc
// => Mock HTTP client
@Autowired
private lateinit var repository: TaskRepository
@BeforeEach
fun setUp() {
repository.deleteAll() // => Clean database before each test
}
@Test
fun `createTask with valid request returns created`() {
val requestBody = """{"title":"Test Task","description":"Test Description"}"""
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
// => POST request with JSON body
.andExpect(status().isCreated)
// => Verify 201 Created
.andExpect(jsonPath("$.title").value("Test Task"))
// => Verify response JSON
.andExpect(jsonPath("$.status").value("pending"))
}
@Test
fun `getTask with existing id returns task`() {
val task = repository.save(Task(title = "Existing", description = "Description"))
// => Save task to database
mockMvc.perform(get("/api/tasks/${task.id}"))
// => GET request
.andExpect(status().isOk)
// => Verify 200 OK
.andExpect(jsonPath("$.title").value("Existing"))
}
@Test
fun `getTask with non-existing id returns not found`() {
mockMvc.perform(get("/api/tasks/999"))
.andExpect(status().isNotFound)
// => Verify 404 Not Found
}
}Run tests:
./mvnw testConcept 11: Production-Ready Features with Actuator
Spring Boot Actuator provides monitoring and management endpoints.
Add Actuator Dependency
Maven (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Gradle (build.gradle.kts):
implementation("org.springframework.boot:spring-boot-starter-actuator")Configure Actuator
File: application.properties
management.endpoints.web.exposure.include=health,info,metrics,env
# => Expose specific endpoints
management.endpoint.health.show-details=always
# => Show detailed health information
info.app.name=Task Manager API
info.app.description=Spring Boot Task Management
info.app.version=1.0.0Built-in Actuator Endpoints
Health Check:
curl http://localhost:8080/actuator/healthResponse:
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "PostgreSQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP"
}
}
}Metrics:
curl http://localhost:8080/actuator/metricsShows available metrics: JVM memory, CPU, HTTP requests, database connections.
Application Info:
curl http://localhost:8080/actuator/infoResponse:
{
"app": {
"name": "Task Manager API",
"description": "Spring Boot Task Management",
"version": "1.0.0"
}
}Custom Health Indicator
Java:
package com.example.taskmanager.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component // => Register as Spring component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Check custom health conditions
boolean isHealthy = checkExternalService();
// => Custom health check logic
if (isHealthy) {
return Health.up() // => Service healthy
.withDetail("service", "Available")
.build();
} else {
return Health.down() // => Service unhealthy
.withDetail("service", "Unavailable")
.build();
}
}
private boolean checkExternalService() {
return true; // => Actual check implementation
}
}Kotlin:
package com.example.taskmanager.health
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.stereotype.Component
@Component // => Register as Spring component
class CustomHealthIndicator : HealthIndicator {
override fun health(): Health {
// Check custom health conditions
val isHealthy = checkExternalService()
// => Custom health check logic
return if (isHealthy) {
Health.up() // => Service healthy
.withDetail("service", "Available")
.build()
} else {
Health.down() // => Service unhealthy
.withDetail("service", "Unavailable")
.build()
}
}
private fun checkExternalService(): Boolean {
return true // => Actual check implementation
}
}Concept 12: Building and Deploying
Package and run Spring Boot application.
Build Executable JAR
Maven:
./mvnw clean package # => Compile, test, package to JARGradle:
./gradlew clean build # => Compile, test, package to JAROutput: target/taskmanager-0.0.1-SNAPSHOT.jar (Maven) or build/libs/taskmanager-0.0.1-SNAPSHOT.jar (Gradle)
Run JAR Directly
java -jar target/taskmanager-0.0.1-SNAPSHOT.jar
# => Start application from JAR
# => No build tool neededJAR includes:
- Compiled classes
- Dependencies
- Embedded Tomcat server
- Application configuration
Docker Deployment
Dockerfile:
FROM eclipse-temurin:21-jre # => Base image with Java 21 runtime
WORKDIR /app # => Set working directory
COPY target/*.jar app.jar # => Copy JAR to container
EXPOSE 8080 # => Expose port 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# => Run JAR on container startBuild and run:
./mvnw clean package
docker build -t taskmanager:1.0 .
docker run -p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/taskdb \
taskmanager:1.0Production Configuration
Environment variables override:
java -jar app.jar \
--spring.datasource.url=jdbc:postgresql://prod-db:5432/taskdb \
--spring.datasource.username=produser \
--spring.datasource.password=prodpass \
--spring.jpa.hibernate.ddl-auto=validateSummary
What you’ve learned:
- Spring Boot Architecture - Three-layer structure (Controller, Service, Repository)
- REST Controllers - Handle HTTP requests with
@RestController,@GetMapping,@PostMapping - Dependency Injection - Automatic object creation with
@Autowiredand constructor injection - DTOs - Separate API representation from domain models
- JPA Entities - Map Java/Kotlin classes to database tables with
@Entity,@Id,@Column - Spring Data Repositories - Database operations without SQL using
JpaRepository - Database Configuration - Configure PostgreSQL connection in
application.properties - Exception Handling - Custom exceptions with
@RestControllerAdviceglobal handler - CRUD Operations - Complete Create, Read, Update, Delete implementation
- Testing - Unit tests with Mockito/MockK, integration tests with MockMvc
- Actuator - Production monitoring with health checks, metrics, endpoints
- Deployment - Build executable JAR, Docker containerization
Key annotations:
@SpringBootApplication- Enable auto-configuration@RestController- REST API controller@Service- Service layer component@Repository- Data access layer component@Entity- JPA database entity@Autowired- Dependency injection@GetMapping,@PostMapping,@PutMapping,@DeleteMapping- HTTP method mapping@PathVariable- Extract URL path variable@RequestBody- Deserialize JSON request body@ExceptionHandler- Handle specific exceptions
Skills gained:
- Building REST APIs with Spring Boot
- Database integration with JPA and PostgreSQL
- Dependency injection and layered architecture
- Error handling and validation
- Testing strategies
- Production deployment
Next Steps
Ready for comprehensive Spring Boot mastery?
- Beginner Tutorial (0-60% coverage) - Deep dive with extensive practice on controllers, services, repositories, security, and advanced features
Prefer code-first learning?
- By-Example Tutorial - Learn through 75-90 heavily annotated examples covering 95% of Spring Boot features
Need setup help?
- Initial Setup - JDK installation, Spring Initializr, first application
Want to understand Spring Boot’s philosophy?
- Overview - Spring Boot ecosystem, use cases, version compatibility