Hãy xem 17 thói quen kiến trúc Spring Boot giúp phân biệt các Senior Devs với những người còn lại

admin . 8:01 am
Thẳng thắn mà nói, Spring Boot giúp việc tạo ứng dụng trở nên dể dàng
Và vì nó quá dể, chúng ta thường bỏ qua những công việc vệ “sinh kiến trúc” nhàm chán trong việc vội vàng ra mắt tính năng. Sau đó quá trình production, bạn đột nhiên phải đối mặt với mớ hỗn độn controller, service, những lỗi bí ẩn xuất hiện lúc 2h sáng. Tôi cũng đã từng như vậy.
Vì vậy tôi sẽ cung cấp cho bạn 15 architecture hacks cho Spring Boot. Đó là những quy tắc thực tế giữ cho code dể bảo trì và an toàn ở môi trường production
1. Cấu hình application config riêng cho từng môi trường
Việc hardcode url, password hoặc những feature flags trong application.properties đang tự chuốc lấy rắc rối. Tôi đã vô tình nhìn thấy cấu hình connect database prodction bởi gì ai đó đã quên đổi cấu hình môi trường.
Tạo file config tác riêng cho từng môi trường:
application-dev.properties
application-test.properties
application-prod.properties
Và kích hoạt chúng theo theo profile
spring:
profiles:
active: prodBenefit:
No accidental “Oops, I dropped the prod DB” moments.
Không có những khoảng khắc vô tính: “Ẹc e đã xóa DB production mất rồi!”
2. Đặt các cấu hình @Configuration vào các class tiêng
Thay vì nhồi tất cả các cấu hình vào 1 file lớn thì hãy tách chúng ra thành các class có annotation @Configuration để dể quản lý và bảo trì
@Configuration
public class SecurityConfig {
// Security-related beans
}
@Configuration
public class MessagingConfig {
// RabbitMQ, Kafka, etc.
}Điều này giữ các bean liên quan 1 các hợp lý. Database config nằm trong class DatabaseConfig, Security bean nằm trong SecurityConfig
3. Giữ cho Controller làm nhiệm vụ điều phối không chứa logic
Nếu controller của bạn có 500 dòng code logic business thì bạn đã sai
Controller chỉ mapping http request, validate input và phân công (delegating) công việc – không được xử lý dữ liệu hay truy cập trực tiếp vào database
Bad:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}
Good:
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUserById(id);
}4. Controller không nên gọi trực tiếp Repository
Việc inject repository trực tiếp vào controllers mà bỏ qua service layer nơi lẽ ra phải nắm business logic
Nó khiến cho việc tái sử dụng khó hơn, khó cho việc quản lý transaction và áp dụng những class kiểm tra security sau này
Bad:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}
}Good:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id).orElseThrow();
return new UserResponse(user.getName(), user.getEmail());
}
}5. Luôn luôn tách biệt DTOs cho request, Entity và Response
Các JPA, JDBC entity không nên được dùng như 1 model api.
Các entity dùng cho mục đích lưu/lấy từ DB. DTO dùng trong logic api. Nếu dùng chùng sẽ chúng bị ràng buộc chặt chẽ và khó dể maintain code
Example:
@Data
public class CreateUserRequest {
private String name;
private String email;
}
@Data
@AllArgsConstructor
public class UserResponse {
private String name;
private String email;
}
@Data
public class CreateUserRequest {
private String name;
private String email;
}
@Data
@AllArgsConstructor
public class UserResponse {
private String name;
private String email;
}
@Table("users")
@Data
public class User {
@Column(value= "name")
private String name;
@Column(value= "email")
private String email;
}6. Tạo Global Exception Handler với @ControllerAdvice
Việc nhồi try-catch ở tất cả các endpoint sẽ làm code rối và khó quản lý lỗi một cách thống nhất.
Dùng Global Exception Handler (qua @ControllerAdvice) giúp gom logic xử lý lỗi về một chỗ, code sạch hơn, dễ bảo trì hơn.
Example:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}7. Dùng HTTP Status Code có ý nghĩa, đừng chỉ trả về mỗi 200 OK.
Việc trả về status code đúng ngữ cảnh giúp API dễ hiểu hơn, client xử lý chính xác hơn và dễ debug hơn.
Dùng:
201 Created → khi tạo resource mới thành công
400 Bad Request → khi input không hợp lệ
401 Unauthorized → khi chưa đăng nhập
403 Forbidden → khi không có quyền truy cập
404 Not Found → khi resource không tồn tại
500 Internal Server Error → khi server gặp lỗi8. Hạn chế dùng static utility class trong business logic.
Static utility class (ví dụ: Utils, Helper với toàn hàm static) có thể tiện lúc đầu, nhưng về lâu dài sẽ gây khó test, khó mock, và khiến code ít linh hoạt.
Thay vào đó, hãy ưu tiên thiết kế theo Dependency Injection (Spring bean/service) để dễ mở rộng, dễ unit test, và clean hơn.
9. Tổ chức code theo feature, không chỉ theo layer.
Cách truyền thống: tách thành package controller/, service/, repository/. Dễ quản lý lúc nhỏ nhưng khi dự án lớn sẽ khó tìm kiếm và khó gắn kết logic của từng tính năng.
Old School:
controller/
service/
repository/
Better:
user/
UserController.java
UserService.java
UserRepository.java
order/
OrderController.java
OrderService.javaNhóm theo tính năng (người dùng, đơn hàng, hàng tồn kho) giúp mã liên quan được gắn kết với nhau và dễ điều hướng hơn.
10. Tuân thủ chặt chẽ luồng Từ Controller ->Service -> Repository
This layer separation is like traffic rules — it prevents chaos.
- Controller: chỉ nhận request, validate input, rồi chuyển tiếp.
- Service: nơi chứa business logic.
- Repository: nơi làm việc trực tiếp với database.
Không được “đi tắt” như Controller gọi thẳng Repository, vì sẽ phá vỡ kiến trúc, gây khó test, khó mở rộng, và code nhanh chóng trở nên rối.
11. Luôn luôn validate request
Tại sao: Dừng bao giờ tin dữ liệu từ client. Sử dụng @Valid và các validation annotations để loại bỏ dữ liệu ngay từ đầu
@PostMapping("/users")
public ResponseEntity<Void> createUser(@Valid @RequestBody CreateUserRequest request) {
userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}12. Sử dụng transaction một cách hợp lý
Đừng bừa bãi gắn @Transactional ở mọi nơi.
Transaction nên được quản lý ở service layer, nơi chứa business logic.
Tránh để transaction “sống quá lâu” (ví dụ: giữ transaction mở xuyên qua nhiều bước xử lý phức tạp hoặc gọi API ngoài).
Đừng gọi method @Transactional từ cùng class
- Vì Spring AOP chỉ tạo proxy bên ngoài → gọi nội bộ sẽ không áp dụng transaction.
- Giải pháp: tách method ra service khác hoặc dùng self-injection.
Xử lý rollback đúng cách
- Spring mặc định rollback khi có
RuntimeExceptionhoặcError. - Nếu muốn rollback cho checked exception:
@Transactional(rollbackFor = Exception.class)
public void doSomething() { ... }Quản lý transaction hợp lý giúp tránh deadlock, giảm lỗi dữ liệu, và tăng hiệu năng.
13. Sử dụng Profiles để quản lý Feature Toggle
Trong Spring Boot, có thể dùng @Profile hoặc config theo profile (application-dev.yml, application-prod.yml, …) để bật/tắt tính năng tùy theo môi trường.
Cách này giúp tránh hardcode flag trong code, đồng thời giữ cho dev/test/prod độc lập.
@Service
@Profile("payment-new")
public class NewPaymentService implements PaymentService {
// Code cho tính năng mới
}
@Service
@Profile("!payment-new")
public class OldPaymentService implements PaymentService {
// Code cũ fallback
}Nhờ đó, bạn có thể bật/tắt tính năng chỉ bằng cách đổi profile thay vì sửa code.
14. Dùng Global Response Wrapper
Thay vì trả về dữ liệu raw
public UserResponse getUser() { ... }Bọc chúng:
public ApiResponse<UserResponse> getUser() { ... }
public class ApiResponse<T> {
private T data;
private String message;
}Cấu trúc thống nhất giúp nhóm front-end và việc ghi nhật ký dễ dàng hơn.
15. Ưu tiên Constructor Injection thay vì Field Injection
Giúp immutability, dễ test, dễ nhận biết dependency bắt buộc, và phù hợp với Spring’s dependency injection.
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
}16. Circular dependency xảy ra khi 2 (hoặc nhiều) service phụ thuộc lẫn nhau
@Service
public class OrderService {
private final PaymentService paymentService;
// ...
}
@Service
public class PaymentService {
private final OrderService orderService;
// ...
}Cách tránh:
Xem lại design: nếu 2 service phụ thuộc trực tiếp vào nhau, có thể đó là dấu hiệu thiết kế chưa chuẩn (chưa phân tách đúng responsibility).
Tách business logic chung ra một service thứ ba (ví dụ: BillingService).
Giảm mức phụ thuộc: một service chỉ expose interface cần thiết thay vì toàn bộ.
17. Log thông minh — ưu tiên ngữ cảnh thay vì spam log
Đừng chỉ log càng nhiều càng tốt (System.out.println khắp nơi hay log đủ mọi thứ).
Thay vào đó, log những gì có ý nghĩa và gắn với ngữ cảnh:
Có lỗi gì bất thường?
Ai gọi API?
Input là gì?
Kết quả xử lý ra sao?
log.info("Creating order for userId={}, productId={}", userId, productId);
log.error("Payment failed for orderId={}, reason={}", orderId, ex.getMessage());Lời khuyên cuối cùng
Kiến trúc phần mềm cũng giống như chế độ ăn uống của bạn vậy — bạn có thể duy trì thói quen xấu một thời gian, nhưng sớm hay muộn, hậu quả sẽ ập đến theo cách tệ nhất.
👉 Hãy áp dụng những best practice này ngay từ đầu, trước khi hệ thống production của bạn “lên tiếng” bằng một bài học đau đớn.

