Layered Architecture in SpringBoot: Nine-Layer Structure and Detailed Implementation
This article explains the concept of layered architecture for Java SpringBoot projects, introduces a nine‑layer structure, discusses each layer’s responsibilities, shows concrete code examples for utilities, infrastructure, domain, service, integration, facade, client, controller and boot modules, and summarizes the five design dimensions of single responsibility, noise reduction, adaptation, business logic, and data purity.
In software engineering, the principle that any problem can be solved by adding a virtual layer highlights the importance of layering, which also applies to Java project architecture. Layering isolates responsibilities, similar to the Single Responsibility Principle, and reduces coupling, though it introduces adapter overhead for inter‑layer communication.
Five design dimensions for layering:
Single: each layer handles one type of concern.
Noise reduction: only necessary information is passed down (Principle of Least Knowledge).
Adaptation: adapters translate information between layers.
Business: business objects can aggregate logic, e.g., using a rich domain model.
Data: data objects remain pure and avoid embedding business logic.
The article proposes a nine‑layer structure for a SpringBoot project:
user-demo-service
├─ util
├─ integration
├─ infrastructure
├─ service
├─ domain
├─ facade
├─ controller
├─ client
└─ boot2. Detailed Layer Implementation
util layer contains independent utility classes:
user-demo-service-util
└─ src/main/java
├─ date/DateUtil.java
├─ json/JSONUtil.java
└─ validate/BizValidator.javainfrastructure layer hosts data access objects and MyBatis mappers for two domains (player and game):
user-demo-service-infrastructure
└─ src/main/java
├─ player/entity/PlayerEntity.java
├─ player/mapper/PlayerEntityMapper.java
├─ game/entity/GameEntity.java
└─ game/mapper/GameEntityMapper.javaSQL for the player table demonstrates keeping the data object pure, storing complex fields like gamePerformance as JSON strings.
CREATE TABLE `player` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`player_id` varchar(256) NOT NULL COMMENT '运动员编号',
`player_name` varchar(256) NOT NULL COMMENT '运动员名称',
`height` int(11) NOT NULL COMMENT '身高',
`weight` int(11) NOT NULL COMMENT '体重',
`game_performance` text COMMENT '最近一场比赛表现',
`creator` varchar(256) NOT NULL COMMENT '创建人',
`updator` varchar(256) NOT NULL COMMENT '修改人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;The corresponding Java entity and mapper are:
public class PlayerEntity {
private Long id;
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private String creator;
private String updator;
private Date createTime;
private Date updateTime;
private String gamePerformance;
}
@Repository
public interface PlayerEntityMapper {
int insert(PlayerEntity record);
int updateById(PlayerEntity record);
PlayerEntity selectById(@Param("playerId") String playerId);
}domain layer follows DDD concepts, distinguishing domain objects from data objects and business objects. Example domain objects:
public class PlayerQueryResultDomain {
private String playerId;
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
}
public class GamePerformanceVO {
private Double runDistance; // 跑动距离
private Double passSuccess; // 传球成功率
private Integer scoreNum; // 进球数
}Business objects (BO) embed validation logic, e.g., PlayerCreateBO implements BizValidator and checks fields before processing.
public class PlayerCreateBO implements BizValidator {
private String playerName;
private Integer height;
private Integer weight;
private GamePerformanceVO gamePerformance;
private MaintainCreateVO maintainInfo;
@Override
public void validate() {
if (StringUtils.isEmpty(playerName)) {
throw new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT);
}
// other checks omitted for brevity
}
}service layer orchestrates domain objects, uses adapters to convert between domain and entity, and sends events via message senders:
public class PlayerService {
@Resource private PlayerEntityMapper playerEntityMapper;
@Resource private PlayerMessageSender playerMessageSender;
@Resource private PlayerServiceAdapter playerServiceAdapter;
public boolean updatePlayer(PlayerUpdateDomain player) {
AssertUtil.notNull(player, new BizException(ErrorCodeBizEnum.ILLEGAL_ARGUMENT));
player.validate();
PlayerEntity entity = playerServiceAdapter.convertUpdate(player);
playerEntityMapper.updateById(entity);
playerMessageSender.sendPlayerUpdatemessage(player);
return true;
}
}Adapters translate domain objects to entities or events:
public class PlayerServiceAdapter {
public PlayerEntity convertUpdate(PlayerUpdateDomain domain) {
PlayerEntity player = new PlayerEntity();
player.setPlayerId(domain.getPlayerId());
player.setPlayerName(domain.getPlayerName());
player.setWeight(domain.getWeight());
player.setHeight(domain.getHeight());
if (domain.getGamePerformance() != null) {
player.setGamePerformance(JacksonUtil.bean2Json(domain.getGamePerformance()));
}
player.setUpdator(domain.getMaintainInfo().getUpdator());
player.setUpdateTime(domain.getMaintainInfo().getUpdateTime());
return player;
}
public PlayerUpdateEvent convertUpdateEvent(PlayerUpdateDomain domain) {
PlayerUpdateEvent event = new PlayerUpdateEvent();
event.setPlayerUpdateDomain(domain);
event.setMessageId(UUID.randomUUID().toString());
event.setMessageId(PlayerMessageType.UPDATE.getMsg());
return event;
}
}integration layer adapts external services (e.g., a user center) into internal domain objects using adapters and proxies.
public class UserClientAdapter {
public UserInfoDomain convertUserDomain(UserInfoClientDTO userInfo) {
UserInfoDomain userDomain = new UserInfoDomain();
UserContactVO contactVO = new UserContactVO();
contactVO.setMobile(userInfo.getMobile());
userDomain.setContactInfo(contactVO);
UserAddressVO addressVO = new UserAddressVO();
addressVO.setCityCode(userInfo.getCityCode());
addressVO.setAddressDetail(userInfo.getAddressDetail());
userDomain.setAddressInfo(addressVO);
return userDomain;
}
}
public class UserClientProxy {
@Resource private UserClientService userClientService;
@Resource private UserClientAdapter userClientAdapter;
public UserInfoDomain getUserInfo(String userId) {
UserInfoClientDTO user = userClientService.getUserInfo(userId);
return userClientAdapter.convertUserDomain(user);
}
}facade and client layers expose a clean API to external callers. The client contains DTOs that hide internal fields, while the facade implements the service logic using domain, service, and client modules.
public class PlayerFacadeAdapter {
public PlayerQueryResultDTO convertQuery(PlayerQueryResultDomain domain) {
if (domain == null) return null;
PlayerQueryResultDTO result = new PlayerQueryResultDTO();
result.setPlayerId(domain.getPlayerId());
result.setPlayerName(domain.getPlayerName());
result.setHeight(domain.getHeight());
result.setWeight(domain.getWeight());
if (domain.getGamePerformance() != null) {
result.setGamePerformanceDTO(convertGamePerformance(domain.getGamePerformance()));
}
return result;
}
}The controller layer maps HTTP requests to the facade services, ensuring that sensitive data such as the logged‑in user ID is obtained from request headers rather than client input.
@RestController
@RequestMapping("/player")
public class PlayerController {
@Resource private PlayerClientService playerClientService;
@PostMapping("/add")
public ResultDTO<Boolean> add(@RequestHeader("test-login-info") String loginUserId,
@RequestBody PlayerCreateDTO dto) {
dto.setCreator(loginUserId);
return playerClientService.addPlayer(dto);
}
// update and query endpoints omitted for brevity
}The boot layer contains only the SpringBoot entry point, scanning all mapper packages.
@MapperScan("com.user.demo.service.infrastructure.*.mapper")
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}Conclusion : The article revisits the five layering dimensions—single responsibility, noise reduction, adaptation, business aggregation, and data purity—demonstrating how a well‑structured nine‑layer SpringBoot project improves maintainability, testability, and scalability.
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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
