Been Writing Spring Controllers for a Decade? Time to Ditch the Boilerplate Pattern
The article argues that traditional Spring Controllers impose heavy, repetitive scaffolding that outweighs business value, and demonstrates how a functional Java API can replace class‑based controllers with simple handler functions, reducing cognitive load, test complexity, and development overhead.
Problem: Heavy Spring Controller Boilerplate
Adding a simple read‑only endpoint often requires creating a controller class, request/response DTOs, a mapper, validation annotation, a unified exception wrapper and starting half a Spring Boot context for tests. A PR example shows seven new items across multiple packages, and review discussions focus on where to place annotations rather than on business logic.
Typical Reflexive Pattern
New requirement → New Controller → New DTO → Validation → Mapper → Unified response → Exception wrapper
Even when the core logic is a single line return userService.findById(id);, the project adopts a directory layout:
/src/main/java
└── com/icoderoad/demo
├── controller
├── service
├── dto
├── vo
├── mapper
└── exceptionThis raises the question whether an interface truly needs so many layers.
Analysis of Boilerplate Ratio
≈80 % of controller code is repetitive boilerplate.
≈15 % is imposed conventions.
≈5 % implements actual business logic.
Consequences:
Tests must start a Spring container.
Changing a single line of logic touches many files.
Code reviews discuss annotations instead of business.
Functional Java API Alternative
Viewing an HTTP endpoint as input → processing → output leads to a function‑based definition.
Traditional Spring Controller
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserVO> findById(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(UserMapper.toVO(user));
}
}Functional API Definition
package com.icoderoad.api.user;
import static com.icoderoad.web.Http.*;
public class UserApi {
public static final Handler getUserById =
GET("/users/{id}", req -> {
Long id = req.pathVar("id", Long.class);
return ok(userService().findById(id));
});
}The only change is replacing the class‑plus‑annotations model with a plain function that returns the result directly, eliminating hidden framework magic.
Testing Comparison
Traditional controller tests require:
Starting the Spring context
Mocking the web layer
Building HTTP requests
Parsing the response
With the functional API the test becomes a simple unit test:
@Test
void should_return_user() {
var response = UserApi.getUserById.handle(fakeRequest("/users/1"));
assertThat(response.status()).isEqualTo(200);
}No container, no framework dependency, and the business logic is directly verifiable, resulting in faster feedback.
When the Functional Style Is Advantageous
Read‑heavy, write‑light endpoints
Internal systems or admin back‑ends
API aggregation layers / gateways
Modules where test execution speed is critical
Key Insight
The issue is not the Spring framework itself but the habit of following a fixed layered template without considering the simplicity of the underlying logic. When the interface logic is simpler than its surrounding structure, the structure should yield.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
