This tutorial demonstrates Thread Injection: the ability for OfficeFloor to automatically assign specific thread pools to steps based on the dependencies those steps require.
All the tutorials so far have focused on what executes — composing service methods, injecting Spring beans, governing transactions. This tutorial introduces who executes each step: the thread pool, or in OfficeFloor's terminology, the Team.
The central idea is simple: a step that depends on a JdbcTemplate is almost certainly going to perform blocking database I/O. OfficeFloor detects this at start-up and routes that step to a dedicated database thread pool automatically. The socket (request) thread is freed immediately and never blocks.
The tutorial exposes a single GET /thread endpoint with two composed steps. The response records which thread executed each step so you can see them differ.
<dependency> <groupId>net.officefloor.springboot</groupId> <artifactId>officefloor-rest-spring-boot-starter</artifactId> </dependency> <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>
@SpringBootApplication
public class SpringRestTeamApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestTeamApplication.class, args);
}
}
Consider a typical REST endpoint that calls a database. With a traditional thread-per-request model the request thread blocks for the entire duration of the database call. Under load, all threads block and the server stalls — even requests that need no database access.
OfficeFloor's Thread Injection solves this by assigning a dedicated thread pool (a Team) to every step that performs blocking I/O. The socket threads that receive HTTP requests hand off to the database team the moment a blocking step is reached, then pick up new requests. When the database call completes the continuation runs on whichever thread is next available.
The benefit compounds: cached reads and lightweight steps never wait behind database calls, and the number of socket threads needed is much smaller because they are almost never blocked.
officefloor/teams/Teams are configured as YAML files in src/main/resources/officefloor/teams/. The file name is the team label. The type field names the fully-qualified class whose presence as a method parameter triggers assignment to this team.
source: net.officefloor.frame.impl.spi.team.ExecutorCachedTeamSource type: org.springframework.jdbc.core.JdbcTemplate
ExecutorCachedTeamSource provides an Executor-backed cached thread pool. Any number of team files may be placed in officefloor/teams/; each one defines an independent pool.
Including the directory is optional. When no team files are present all steps execute on the default team (the socket thread), which is the correct behaviour for CPU-bound or non-blocking work.
The REST YAML composes two steps:
request: class: net.officefloor.tutorial.springrestteam.RequestThreadService next: query query: class: net.officefloor.tutorial.springrestteam.DatabaseQueryService
The first step (request) runs on the socket thread. Its return value — the socket thread name — is passed as a @Parameter to the second step (query).
The second step (query) has a JdbcTemplate dependency and is therefore automatically routed to the JdbcTemplate team. It records the database thread name, performs a query, and writes the response.
RequestThreadService is a plain Java class. It captures the current thread name and returns it. Because it has no blocking dependencies it always runs on the socket thread:
public class RequestThreadService {
public String captureThread() {
return Thread.currentThread().getName();
}
}
DatabaseQueryService is a Spring @Service with an auto-wired JdbcTemplate. Because JdbcTemplate is named in the team file, OfficeFloor assigns this step to the database thread pool automatically — no annotation or explicit wiring is needed beyond the dependency itself:
public class DatabaseQueryService {
public void query(@Parameter String socketThread, JdbcTemplate jdbcTemplate,
ObjectResponse<ThreadDemoResponse> response) {
String databaseThread = Thread.currentThread().getName();
int tableCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM information_schema.tables", Integer.class);
response.send(new ThreadDemoResponse(socketThread, databaseThread, tableCount));
}
}
The @Parameter annotation receives the socket thread name passed from step 1.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ThreadDemoResponse {
private String socketThread;
private String databaseThread;
private int tableCount;
}
A typical response when both teams are active:
{
"socketThread" : "ofhttp-3",
"databaseThread" : "JdbcTemplate-0",
"tableCount" : 12
}The thread names are different, proving that the blocking database call never ran on the socket thread.
@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestTeamHttpServerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper mapper;
@Test
public void threadDemoReturnsThreadNames() throws Exception {
mvc.perform(get("/thread").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.socketThread").isString())
.andExpect(jsonPath("$.databaseThread").isString())
.andExpect(jsonPath("$.tableCount").isNumber());
}
@Test
public void differentThreadsServiceEachStep() throws Exception {
MvcResult result = mvc.perform(get("/thread").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
ThreadDemoResponse response = mapper.readValue(
result.getResponse().getContentAsString(), ThreadDemoResponse.class);
assertNotNull(response.getSocketThread(), "socketThread must be populated");
assertNotNull(response.getDatabaseThread(), "databaseThread must be populated");
// With team support active, the blocking database step runs on a dedicated
// thread pool — socket threads are never blocked waiting on the database.
assertNotEquals(response.getSocketThread(), response.getDatabaseThread(),
"socket thread and database thread should differ once team support is active");
}
}
threadDemoReturnsThreadNames verifies the endpoint returns a valid response. differentThreadsServiceEachStep asserts that socketThread and databaseThread are different thread names — demonstrating that the blocking step was routed away from the socket thread.
After completing this tutorial you can:
The Spring Boot application is still the container. All Spring features remain available. The only additions are the YAML files in officefloor/teams/.
The Spring Actuator tutorial demonstrates that Spring Boot Actuator health, info, and metrics endpoints continue to work unchanged alongside OfficeFloor REST YAML endpoints.