Java Layered Architecture Design Pattern (Spring Boot + MyBatis-Plus)
A practical guide to implementing a classic five-layer architecture with Spring Boot and MyBatis-Plus.
When to Apply
Use this skill in the following scenarios:
- Creating a new Java Web project
- Refactoring an existing project architecture
- Designing a multi-module Java system
- Checking architectural compliance during code reviews
- Defining architectural standards for team development
- Database migration or technology stack migration
Assumptions
- Spring Boot 3.x
- MyBatis-Plus for data access
- DTO/VO separation at API boundaries
Quick Reference
| Priority | Category | Impact Area | Prefix |
|---|---|---|---|
| 1 | Separation of Concerns | Architectural clarity | -principle- |
| 2 | Unidirectional Dependency | Dependency management | -dependency- |
| 3 | Interface Abstraction | Replaceability | -abstraction- |
| 4 | Data Transfer | API consistency | -data- |
| 5 | Exception Handling | Error handling | -exception- |
| 6 | Cross-Layer Invocation | Architectural compliance | -crosslayer- |
| 7 | Reverse Dependency | Architectural compliance | -reverse- |
| 8 | Entity Exposure | Security | -entity-exposure- |
Core Principles
1. Separation of Concerns (SoC)
Each layer should solve one category of problems and must not take on responsibilities belonging to other layers.
// Correct: clear responsibilities per layer
// Presentation layer: HTTP handling only
@RestController
public class ProjectController {
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping("/{id}")
public ApiReturn<ProjectVo> getProject(@PathVariable Long id) {
Project project = projectService.getProject(id);
ProjectVo vo = new ProjectVo();
BeanUtils.copyProperties(project, vo);
return ApiReturn.of(vo);
}
}
// Business logic layer: business rules only
@Service
public class ProjectService {
private final ProjectMapper projectMapper;
public ProjectService(ProjectMapper projectMapper) {
this.projectMapper = projectMapper;
}
public Project getProject(Long id) {
Project project = projectMapper.selectById(id);
if (project == null) {
throw new BusinessException(404, "Project does not exist");
}
return project;
}
}
// Data access layer: data interfaces only
public interface ProjectMapper extends BaseMapper<Project> {
}
2. Unidirectional Dependency
Upper layers may call lower layers, but lower layers must not depend on upper layers.
// Correct: depend on a lower layer abstraction
@Service
public class ProjectService {
private final ProjectMapper projectMapper;
public ProjectService(ProjectMapper projectMapper) {
this.projectMapper = projectMapper;
}
}
// Incorrect: cross-layer invocation
@RestController
public class ProjectController {
private final ProjectMapper projectMapper;
public ProjectController(ProjectMapper projectMapper) {
this.projectMapper = projectMapper;
}
}
3. Interface Abstraction
Layers should interact via interfaces, not concrete implementations.
// Correct: mapper interface managed by MyBatis-Plus
public interface ProjectMapper extends BaseMapper<Project> {
}
// Incorrect: depending on a non-existent implementation class
public class ProjectService {
private ProjectMapperImpl projectMapperImpl;
}
Architecture Layers
Classic Five-Layer Architecture
Presentation Layer → Business Logic Layer → Data Access Layer → Persistence Layer → Database Layer
| Layer | Spring Stack | Common Names | Core Responsibilities |
|---|---|---|---|
| Presentation Layer | @Controller | Controller, API | Handle HTTP requests and responses |
| Business Logic Layer | @Service | Service | Implement business rules and workflows |
| Data Access Layer | @Mapper | Mapper, DAO | Define DB access interfaces |
| Persistence Layer | MyBatis XML, Entity | Mapper XML, Entity | SQL mapping and entity definitions |
| Database Layer | MySQL | Database | Store and retrieve data |
Layer Definitions
Presentation Layer
Responsibilities:
- Handle HTTP requests and responses
- Receive and validate parameters
- Routing and dispatching
- Data format transformation (DTO ↔ Entity ↔ VO)
- Exception handling and HTTP status mapping Template:
@RestController
@RequestMapping("/api/projects")
public class ProjectController {
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping("/{id}")
public ApiReturn<ProjectVo> getProject(@PathVariable Long id) {
Project project = projectService.getProject(id);
ProjectVo vo = new ProjectVo();
BeanUtils.copyProperties(project, vo);
return ApiReturn.of(vo);
}
@PostMapping
public ApiReturn<ProjectVo> createProject(@Valid @RequestBody ProjectCreateRequest request) {
Project project = new Project();
BeanUtils.copyProperties(request, project);
Project created = projectService.createProject(project);
ProjectVo vo = new ProjectVo();
BeanUtils.copyProperties(created, vo);
return ApiReturn.of(vo);
}
}
Design notes:
- Keep it thin: HTTP-related logic only
- Use DTO/VO for data transfer
- Centralize validation with Bean Validation and global exception handling
- No business logic
- No direct database access
Business Logic Layer
Responsibilities:
- Implement business rules and workflows
- Data validation and business checks
- Authorization and access control
- Transaction management (boundary here)
- Coordinate multiple services and mappers
- Call the data access layer Template:
@Service
public class ProjectService {
private final ProjectMapper projectMapper;
public ProjectService(ProjectMapper projectMapper) {
this.projectMapper = projectMapper;
}
public Project getProject(Long id) {
if (id == null || id <= 0) {
throw new BusinessException(400, "Invalid project ID");
}
Project project = projectMapper.selectById(id);
if (project == null) {
throw new BusinessException(404, "Project does not exist");
}
return project;
}
@Transactional
public Project createProject(Project project) {
if (project.getCustomerId() == null) {
throw new BusinessException(400, "Customer ID must not be null");
}
project.setCreatedTime(LocalDateTime.now());
project.setStatus(ProjectStatus.CREATED);
projectMapper.insert(project);
return project;
}
}
Design notes:
- Contains core business logic
- Use
@Transactionalfor transactions - Perform business validations
- Orchestrate multiple services and mappers
- No HTTP concerns
- No direct DB connection handling
- Service should not return DTO (DTO is an input model). Return Entity/Domain or VO consistently.
Data Access Layer (MyBatis-Plus)
Responsibilities:
- Define DB access interfaces
- Provide CRUD abstractions
- Encapsulate persistence details
- Provide aggregate query interfaces when needed Template:
public interface ProjectMapper extends BaseMapper<Project> {
}
Design notes:
- Interfaces only; no concrete implementations
- Method names clearly express intent
- No SQL statements here (use XML for complex SQL)
- No business logic
- No transaction management
- Must not depend on the Service layer
Persistence Layer
Responsibilities:
- SQL mapping and result mapping
- Entity definitions
- Database-specific SQL fragments (when needed) Template (MyBatis-Plus XML):
<mapper namespace="com.example.mapper.ProjectMapper">
<select id="selectActive" resultType="com.example.entity.Project">
SELECT * FROM project WHERE status = 'CREATED'
</select>
</mapper>
Design notes:
- Focus on SQL authoring and optimization
- Result mapping and type conversion
- Use SQL fragments for reusability
- No business logic
- No business validations
- Connection management and transaction boundaries are handled by Spring and DataSource
Database Layer
Responsibilities:
- Persistent storage
- Data integrity constraints
- Indexes and performance optimization
- Transaction support Template (MySQL):
CREATE TABLE project (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
customer_id BIGINT NOT NULL,
status VARCHAR(50) DEFAULT 'CREATED'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Data Transfer Patterns
Using DTO, VO, and Entity
Frontend → Controller → Service → Mapper → Database
DTO Entity Entity
VO Entity
Conversion Example
@PostMapping("/projects")
public ApiReturn<ProjectVo> createProject(@Valid @RequestBody ProjectCreateRequest request) {
Project project = new Project();
BeanUtils.copyProperties(request, project);
Project created = projectService.createProject(project);
ProjectVo vo = new ProjectVo();
BeanUtils.copyProperties(created, vo);
return ApiReturn.of(vo);
}
Exception Handling
Centralized Exception Handling
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiReturn<Void> handleBusinessException(BusinessException e) {
return ApiReturn.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiReturn<Void> handleException(Exception e) {
return ApiReturn.error(500, "Internal server error");
}
}
Transaction Guidelines
- Transaction boundaries belong to Service methods.
- Use
@Transactional(readOnly = true)for read-only queries. - Rollback on RuntimeException by default; document checked-exception rollbacks if needed.
- Avoid transactions in Controller, Mapper, or XML.
Common Pitfalls
Pitfall 1: Cross-Layer Calls
// Incorrect: Controller directly calls Mapper
@RestController
public class ProjectController {
private final ProjectMapper projectMapper;
public ProjectController(ProjectMapper projectMapper) {
this.projectMapper = projectMapper;
}
public List<Project> getProjects() {
return projectMapper.selectList(null);
}
}
Pitfall 2: Reverse Dependency
// Incorrect: Service depends on Controller
@Service
public class ProjectService {
private ProjectController projectController;
}
Pitfall 3: Business Logic in SQL
<!-- Incorrect: business logic embedded in SQL -->
<select id="getById" resultMap="BaseResultMap">
SELECT * FROM project
WHERE id = #{id}
AND is_deleted = 0
AND has_permission(#{currentUserId}, id) = 1
</select>
Pitfall 4: Returning Entity Directly from Controller
// Incorrect: Controller returns Entity directly
@GetMapping("/projects/{id}")
public Project getProject(@PathVariable Long id) {
return projectService.getProject(id);
}
Project Structure Template (Recommended)
src/main/java/com/xdyai/backend/
controller/
internal/
request/
service/
common/
context/
integration/
mapper/
entity/
enumeration/
src/main/resources/
application.yml
application-dev.yml
mapper/
db/
validation/
integration/
Best Practices Summary
Presentation Layer
- Keep it thin: HTTP logic only
- Use DTO/VO for data transfer
- Centralized exception handling
- No business logic
- No direct Mapper access
Business Logic Layer
- Contains core business logic
- Use
@Transactionalto manage transactions - Orchestrate multiple services
- No HTTP concerns
- No direct database connection handling
Data Access Layer
- Interfaces only
- Clear method naming
- CRUD via MyBatis-Plus
- No SQL statements here
- No business logic
Persistence Layer
- Focus on SQL authoring
- Use SQL fragments
- Use dynamic SQL for complex queries
- No business logic
- No business validations
Cross-Layer Communication
- Controller ← DTO
- Service ← Entity
- Mapper ← Entity
- Controller ← VO
- Avoid cross-layer calls
- Avoid reverse dependencies