best-java-structure
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
More from chasepassion/skills
fastapi-structure-guide
Trigger when the user wants to create a new FastAPI project, add new features, refactor code, or asks about architectural best practices. This skill enforces 2026 clean architecture with SQLModel, Repository Pattern, full async, and production-ready workflow.
6compact
Create a structured continuation handoff checkpoint for long coding or technical conversations when context is tight, the user asks to compact, another model will continue the task, or a clean resume point is needed. Preserve exact technical state, decisions, file paths, commands, blockers, failed attempts, validation status, and next steps. Do not use for generic summaries, meeting notes, or polished end-user documentation.
3bug-fix
Used for locating, reproducing, validating, and fixing software defects. Applicable when the user asks to "fix a bug," "locate an error," "analyze and fix an error," "reproduce an issue," "find the root cause," and similar scenarios.
3log-build
Add or refine sparse, structured, file-persisted application logs for non-trivial code changes. Use when Agent is proposing an implementation approach, writing a plan, or implementing or modifying complex business logic, critical state transitions, validation or parsing flows, boundary interactions (HTTP, DB, cache, queue, filesystem, subprocess), retries, fallbacks, async jobs, concurrency, or background tasks, because planning is the right time to decide log placement. Reuse the project's existing logging infrastructure whenever possible. Do not use for trivial edits, simple obvious CRUD, blanket "log everything" requests, or low-value noise.
3fix-bug
Used for locating, reproducing, validating, and fixing software defects. Applicable when the user asks to “fix a bug,” “locate an error,” “analyze and fix an error” “reproduce an issue,” “find the root cause,” and similar scenarios.
2express-improve
Helps users design, optimize, and review the structure and content of speeches, presentations, and persuasive communication in a wide range of high-stakes scenarios. Applicable situations include, but are not limited to: startup pitches and co-founder recruiting, research presentations and thesis defenses, job talks and academic interviews, product demos and investor pitches, lab meetings and progress updates, public speaking and TEDx-style talks, conference presentations and panel remarks, oral exams and qualifying defenses, expressing viewpoints and persuading others, upward reporting and performance reviews, and internal proposals and solution walkthroughs.
2