Understand servlet containers, blocking vs reactive architectures, and when to use each approach.
Same Task API three ways:
Then load test and compare!
Blocking (Thread-per-Request):
Request 1 → Thread 1 (blocked on I/O)
Request 2 → Thread 2 (blocked on I/O)
Request 3 → Thread 3 (blocked on I/O)
...
Request 200 → Need 200 threads!
Reactive (Event Loop):
Request 1 ──┐
Request 2 ──┤
Request 3 ──┼→ Event Loop (few threads) → Non-blocking I/O
Request 4 ──┤
Request 5 ──┘
Build Task API with raw servlets to understand Spring's abstraction.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>servlet-task-api</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<finalName>servlet-task-api</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/example/servlet/model/Task.java:
package com.example.servlet.model;
public class Task {
private Long id;
private String title;
private String description;
private String status;
public Task() {}
public Task(Long id, String title, String description, String status) {
this.id = id;
this.title = title;
this.description = description;
this.status = status;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
src/main/java/com/example/servlet/repository/TaskRepository.java:
package com.example.servlet.repository;
import com.example.servlet.model.Task;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class TaskRepository {
private static final TaskRepository INSTANCE = new TaskRepository();
private final Map<Long, Task> tasks = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
private TaskRepository() {
// Initialize with sample data
createTask(new Task(null, "Sample Task", "Description", "TODO"));
}
public static TaskRepository getInstance() {
return INSTANCE;
}
public List<Task> findAll() {
return new ArrayList<>(tasks.values());
}
public Optional<Task> findById(Long id) {
return Optional.ofNullable(tasks.get(id));
}
public Task createTask(Task task) {
task.setId(idGenerator.getAndIncrement());
tasks.put(task.getId(), task);
return task;
}
public boolean deleteTask(Long id) {
return tasks.remove(id) != null;
}
}
src/main/java/com/example/servlet/TaskServlet.java:
package com.example.servlet;
import com.example.servlet.model.Task;
import com.example.servlet.repository.TaskRepository;
import com.google.gson.Gson;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.util.stream.Collectors;
@WebServlet(name = "TaskServlet", urlPatterns = {"/api/tasks", "/api/tasks/*"})
public class TaskServlet extends HttpServlet {
private final TaskRepository repository = TaskRepository.getInstance();
private final Gson gson = new Gson();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo();
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
if (pathInfo == null || pathInfo.equals("/")) {
// Get all tasks
String json = gson.toJson(repository.findAll());
resp.getWriter().write(json);
} else {
// Get task by ID
Long id = Long.parseLong(pathInfo.substring(1));
repository.findById(id).ifPresentOrElse(
task -> {
try {
resp.getWriter().write(gson.toJson(task));
} catch (IOException e) {
throw new RuntimeException(e);
}
},
() -> resp.setStatus(HttpServletResponse.SC_NOT_FOUND)
);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Read request body
String body = req.getReader().lines().collect(Collectors.joining());
Task task = gson.fromJson(body, Task.class);
// Create task
Task created = repository.createTask(task);
// Send response
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.setStatus(HttpServletResponse.SC_CREATED);
resp.getWriter().write(gson.toJson(created));
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo();
if (pathInfo != null && !pathInfo.equals("/")) {
Long id = Long.parseLong(pathInfo.substring(1));
boolean deleted = repository.deleteTask(id);
if (deleted) {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
} else {
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
}
}
src/main/java/com/example/servlet/LoggingFilter.java:
package com.example.servlet;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = "/api/*")
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("LoggingFilter initialized");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
long startTime = System.currentTimeMillis();
System.out.println("Request: " + httpRequest.getMethod() + " " +
httpRequest.getRequestURI());
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Response time: " + duration + "ms");
}
@Override
public void destroy() {
System.out.println("LoggingFilter destroyed");
}
}
Build WAR:
mvn clean package
Copy to Tomcat:
cp target/servlet-task-api.war $TOMCAT_HOME/webapps/
Test:
curl http://localhost:8080/servlet-task-api/api/tasks
Build same API with Spring MVC and JDBC.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<groupId>com.example</groupId>
<artifactId>blocking-task-api</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
src/main/java/com/example/blocking/model/Task.java:
package com.example.blocking.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
private Long id;
private String title;
private String description;
private String status;
}
src/main/java/com/example/blocking/repository/TaskRepository.java:
package com.example.blocking.repository;
import com.example.blocking.model.Task;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
@RequiredArgsConstructor
public class TaskRepository {
private final JdbcTemplate jdbcTemplate;
private final RowMapper<Task> taskRowMapper = (rs, rowNum) -> new Task(
rs.getLong("id"),
rs.getString("title"),
rs.getString("description"),
rs.getString("status")
);
public List<Task> findAll() {
return jdbcTemplate.query(
"SELECT * FROM tasks",
taskRowMapper
);
}
public Optional<Task> findById(Long id) {
List<Task> tasks = jdbcTemplate.query(
"SELECT * FROM tasks WHERE id = ?",
taskRowMapper,
id
);
return tasks.isEmpty() ? Optional.empty() : Optional.of(tasks.get(0));
}
public Task save(Task task) {
if (task.getId() == null) {
jdbcTemplate.update(
"INSERT INTO tasks (title, description, status) VALUES (?, ?, ?)",
task.getTitle(), task.getDescription(), task.getStatus()
);
Long id = jdbcTemplate.queryForObject(
"SELECT LAST_INSERT_ID()",
Long.class
);
task.setId(id);
} else {
jdbcTemplate.update(
"UPDATE tasks SET title = ?, description = ?, status = ? WHERE id = ?",
task.getTitle(), task.getDescription(), task.getStatus(), task.getId()
);
}
return task;
}
public void deleteById(Long id) {
jdbcTemplate.update("DELETE FROM tasks WHERE id = ?", id);
}
}
src/main/java/com/example/blocking/service/TaskService.java:
package com.example.blocking.service;
import com.example.blocking.model.Task;
import com.example.blocking.repository.TaskRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class TaskService {
private final TaskRepository repository;
public List<Task> getAllTasks() {
log.info("Getting all tasks - Thread: {}", Thread.currentThread().getName());
simulateSlowOperation(); // Simulate I/O delay
return repository.findAll();
}
public Optional<Task> getTaskById(Long id) {
log.info("Getting task {} - Thread: {}", id, Thread.currentThread().getName());
simulateSlowOperation();
return repository.findById(id);
}
public Task createTask(Task task) {
log.info("Creating task - Thread: {}", Thread.currentThread().getName());
simulateSlowOperation();
return repository.save(task);
}
public void deleteTask(Long id) {
log.info("Deleting task {} - Thread: {}", id, Thread.currentThread().getName());
simulateSlowOperation();
repository.deleteById(id);
}
private void simulateSlowOperation() {
try {
Thread.sleep(100); // Simulate slow database/external API
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
src/main/java/com/example/blocking/controller/TaskController.java:
package com.example.blocking.controller;
import com.example.blocking.model.Task;
import com.example.blocking.service.TaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
@GetMapping
public ResponseEntity<List<Task>> getAllTasks() {
return ResponseEntity.ok(taskService.getAllTasks());
}
@GetMapping("/{id}")
public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Task> createTask(@RequestBody Task task) {
return ResponseEntity.ok(taskService.createTask(task));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
return ResponseEntity.noContent().build();
}
}
src/main/resources/application.yml:
spring:
application:
name: blocking-task-api
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
server:
port: 8081
tomcat:
threads:
max: 200 # Default thread pool size
logging:
level:
com.example.blocking: INFO
src/main/resources/schema.sql:
CREATE TABLE tasks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL
);
INSERT INTO tasks (title, description, status) VALUES
('Sample Task 1', 'Description 1', 'TODO'),
('Sample Task 2', 'Description 2', 'IN_PROGRESS');
mvn spring-boot:run
curl http://localhost:8081/api/tasks
Build same API with Spring WebFlux and R2DBC.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<groupId>com.example</groupId>
<artifactId>reactive-task-api</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
src/main/java/com/example/reactive/model/Task.java:
package com.example.reactive.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table("tasks")
public class Task {
@Id
private Long id;
private String title;
private String description;
private String status;
}
src/main/java/com/example/reactive/repository/TaskRepository.java:
package com.example.reactive.repository;
import com.example.reactive.model.Task;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TaskRepository extends ReactiveCrudRepository<Task, Long> {
// Reactive CRUD methods inherited
}
src/main/java/com/example/reactive/service/TaskService.java:
package com.example.reactive.service;
import com.example.reactive.model.Task;
import com.example.reactive.repository.TaskRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Service
@RequiredArgsConstructor
@Slf4j
public class TaskService {
private final TaskRepository repository;
public Flux<Task> getAllTasks() {
log.info("Getting all tasks - Thread: {}", Thread.currentThread().getName());
return repository.findAll()
.delayElements(Duration.ofMillis(100)); // Simulate I/O delay
}
public Mono<Task> getTaskById(Long id) {
log.info("Getting task {} - Thread: {}", id, Thread.currentThread().getName());
return repository.findById(id)
.delayElement(Duration.ofMillis(100));
}
public Mono<Task> createTask(Task task) {
log.info("Creating task - Thread: {}", Thread.currentThread().getName());
return repository.save(task)
.delayElement(Duration.ofMillis(100));
}
public Mono<Void> deleteTask(Long id) {
log.info("Deleting task {} - Thread: {}", id, Thread.currentThread().getName());
return repository.deleteById(id)
.delayElement(Duration.ofMillis(100));
}
}
src/main/java/com/example/reactive/controller/TaskController.java:
package com.example.reactive.controller;
import com.example.reactive.model.Task;
import com.example.reactive.service.TaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
@GetMapping
public Flux<Task> getAllTasks() {
return taskService.getAllTasks();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Task>> getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<Task> createTask(@RequestBody Task task) {
return taskService.createTask(task);
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteTask(@PathVariable Long id) {
return taskService.deleteTask(id)
.then(Mono.just(ResponseEntity.noContent().<Void>build()));
}
}
src/main/resources/application.yml:
spring:
application:
name: reactive-task-api
r2dbc:
url: r2dbc:h2:mem:///testdb
server:
port: 8082
logging:
level:
com.example.reactive: INFO
io.r2dbc: DEBUG
src/main/resources/schema.sql:
CREATE TABLE IF NOT EXISTS tasks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL
);
src/main/java/com/example/reactive/config/DataInitializer.java:
package com.example.reactive.config;
import com.example.reactive.model.Task;
import com.example.reactive.repository.TaskRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final TaskRepository repository;
@Override
public void run(String... args) {
repository.deleteAll()
.thenMany(Flux.just(
new Task(null, "Sample Task 1", "Description 1", "TODO"),
new Task(null, "Sample Task 2", "Description 2", "IN_PROGRESS")
))
.flatMap(repository::save)
.subscribe();
}
}
mvn spring-boot:run
curl http://localhost:8082/api/tasks
Load test both implementations and compare performance.
Download from: https://jmeter.apache.org/download_jmeter.cgi
blocking-load-test.jmx:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Blocking API Load Test">
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments">
<collectionProp name="Arguments.arguments"/>
</elementProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Users">
<stringProp name="ThreadGroup.num_threads">500</stringProp>
<stringProp name="ThreadGroup.ramp_time">10</stringProp>
<stringProp name="ThreadGroup.duration">60</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET Tasks">
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8081</stringProp>
<stringProp name="HTTPSampler.path">/api/tasks</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
</value>
</objProp>
</ResultCollector>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
Same structure, change port to 8082.
Terminal 1 - Start Blocking API:
cd blocking-task-api
mvn spring-boot:run
Terminal 2 - Run JMeter Test:
jmeter -n -t blocking-load-test.jmx -l blocking-results.jtl -e -o blocking-report/
Terminal 3 - Start Reactive API:
cd reactive-task-api
mvn spring-boot:run
Terminal 4 - Run JMeter Test:
jmeter -n -t reactive-load-test.jmx -l reactive-results.jtl -e -o reactive-report/
During tests, monitor:
# CPU and Memory
top
# Thread count
jstack <pid> | grep "java.lang.Thread.State" | wc -l
# Heap usage
jstat -gc <pid> 1000
Blocking API (Spring MVC):
Reactive API (Spring WebFlux):
Metric | Blocking (MVC) | Reactive (WebFlux) |
Thread Count | 200+ | 10-20 |
Requests/sec | 150 | 400 |
Avg Latency | 500ms | 200ms |
95th Percentile | 1200ms | 400ms |
Memory (Heap) | 512MB | 256MB |
CPU Usage | 60% | 40% |
Analyze thread behavior in both models.
Blocking API:
jstack <pid> > blocking-threads.txt
Example blocking threads:
"http-nio-8081-exec-1" #25 daemon prio=5 os_prio=0
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.example.blocking.service.TaskService.simulateSlowOperation
"http-nio-8081-exec-2" #26 daemon prio=5 os_prio=0
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
... (200 threads)
Reactive API:
jstack <pid> > reactive-threads.txt
Example reactive threads:
"reactor-http-nio-2" #14 daemon prio=5 os_prio=0
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPoll.wait(Native Method)
"reactor-http-nio-3" #15 daemon prio=5 os_prio=0
java.lang.Thread.State: RUNNABLE
... (10-20 threads total)
Blocking:
Reactive:
Decision matrix for choosing the right stack.
✅ Good for:
Example:
✅ Good for:
Example:
Concurrency Level vs Throughput
Blocking: Reactive:
^ ^
| / |
Req | / |
/sec| / | /
| / | /
+-------> | /
Users +------->
Users
Blocking degrades with high concurrency
Reactive scales better with concurrency
Blocking:
Reactive:
Understanding Spring's unified stack approach.
Spring Framework 5+
├── Spring MVC (Servlet Stack)
│ ├── Tomcat/Jetty (Servlet Container)
│ ├── Spring MVC
│ ├── Spring Data JPA
│ └── JDBC
│
└── Spring WebFlux (Reactive Stack)
├── Netty/Undertow (Reactive Server)
├── Spring WebFlux
├── Spring Data R2DBC
└── R2DBC
Both share:
- Spring Core (IoC, DI)
- Spring Boot Auto-configuration
- Spring Security
- Spring Cloud
Same annotations, different execution:
// Both work the same from API perspective
// Blocking
@GetMapping("/tasks")
public List<Task> getTasks() {
return service.findAll();
}
// Reactive
@GetMapping("/tasks")
public Flux<Task> getTasks() {
return service.findAll();
}
// Mix blocking and reactive in same app
@RestController
public class HybridController {
// Blocking endpoint
@GetMapping("/users")
public List<User> getUsers() {
return userService.findAll(); // JDBC
}
// Reactive endpoint
@GetMapping("/notifications")
public Flux<Notification> getNotifications() {
return notificationService.findAll(); // R2DBC
}
}
Congratulations! 🎉 You've mastered servlet containers, blocking, and reactive architectures!
Use Blocking When:
Use Reactive When:
You've completed all 20 codelabs: