Layered Spring Boot Architecture
Place HTTP mapping, validation, business orchestration, transactions, MyBatis SQL, and models in explicit layers with inward dependency direction.
Learning objectives
- Assign one clear responsibility to each application layer
- Keep transport and persistence models from leaking into business policy
What it is
Layered architecture separates an HTTP adapter, application use cases, and persistence details. In this curriculum the practical flow is Controller → Service → Mapper → SQL → Database, with mapping at boundaries and dependencies pointing toward business policy.
Why it matters
Clear boundaries make a Spring application easier to test, review, change, and operate. They also prevent framework input, database columns, and SQL behavior from becoming the public API or the place where business decisions hide.
How it works
Primary dependency flow
| Layer | Owns | Does not own |
|---|---|---|
| Controller | HTTP input/output, status, validation trigger | Business rules or SQL |
| Service | Use case, authorization context, orchestration, transaction boundary | Servlet objects or XML mapping |
| Mapper | Typed persistence operations | HTTP responses or cross-use-case policy |
| SQL | Set operations, joins, locking, affected-row semantics | Presentation shape |
| Database | Constraints, durability, isolation, indexing | Client-specific error copy |
Simple example
com.henheang.task
├── api/
│ ├── TaskController.java
│ ├── CreateTaskRequest.java
│ └── TaskResponse.java
├── application/
│ └── TaskService.java
├── domain/
│ ├── Task.java
│ └── TaskStatus.java
├── persistence/
│ ├── TaskMapper.java
│ └── TaskRow.java
└── support/
└── ApiExceptionHandler.java
resources/mappers/TaskMapper.xmlPackage by feature first when it keeps related code navigable; use subpackages to make layer boundaries visible. A giant global controller/service/repository package tree becomes harder to own as features grow.
Backend example
@Service
public class TaskService {
private final TaskMapper taskMapper;
public TaskService(TaskMapper taskMapper) {
this.taskMapper = taskMapper;
}
@Transactional
public TaskResponse create(CreateTaskCommand command, long actorId) {
Task task = Task.create(command.title(), command.dueAt(), actorId);
TaskRow row = TaskRow.from(task);
taskMapper.insert(row);
return TaskResponse.from(row);
}
}Production example
- 1
HTTP
Controller accepts a validated request and authenticated principal, calls one use case, and maps the result to HTTP.
- 2
Policy
Service enforces ownership and state transition, then defines the atomic transaction boundary.
- 3
Persistence
Mapper executes parameterized SQL and returns affected rows or typed results; database constraints arbitrate concurrent validity.
- 4
Failure
Exceptions cross into a global handler that maps known categories to stable RFC 9457 responses and logs unexpected causes once.
Common mistakes
- Putting business rules and mapper calls directly in controllers
- Returning mapper row objects as the API contract
- Using field injection
- Opening transactions in controllers
- Catching every exception in every layer
- Adding pass-through layers with no boundary or ownership value
Best practices
- Use constructor injection and final dependencies
- Keep controllers thin and services use-case focused
- Validate syntax at the edge and business rules in the service/domain
- Keep SQL explicit and reviewable
- Map expected failures once at the API boundary
- Test layers according to their real responsibility
Trade-offs
| Design | Advantage | Cost |
|---|---|---|
| Separate DTO/domain/row models | Strong compatibility and persistence boundaries | Mapping code |
| Shared simple model | Less code for a small stable feature | Transport and schema changes become coupled |
| Feature packages | Ownership and navigation | Cross-cutting conventions need discipline |
| Global layer packages | Obvious layer inventory | Features scatter across the codebase |
Interview questions
- What belongs in a controller versus a service?
- Why prefer constructor injection?
- Should a DTO be the same as a database row?
- Where should authorization and transactions live?
- When is an extra interface useful rather than ceremonial?
Hands-on task
- 1
Sketch
Draw the Task create and list flows with layer inputs, outputs, and failure types.
- 2
Implement
Build controller, service, mapper, XML, schema, DTO mapping, and global error handling.
- 3
Review
Write one sentence for each class responsibility; split or remove any class that cannot be explained clearly.
References
- Spring: Spring Boot 3.5.16 System Requirements — https://docs.spring.io/spring-boot/3.5/system-requirements.html
- Spring: Dependency Injection — https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
- Spring: Annotated Controllers — https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller.html
- MyBatis: MyBatis Spring Boot Starter Reference — https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
Retrieval check
Before you continue
- Explain the main idea in your own words without rereading the page.
- Name one production problem this knowledge helps prevent.
- Describe where you would apply it in a Spring Boot project.
Source metadata
Primary references checked on 2026-07-16. Version scope is stated at the top of this page.