99 Ngày Spring — Ngày 98: Best Practices Tổng hợp
Sau 97 ngày, phần khó không còn là biết thêm một annotation mà là dùng nhất quán những gì đã biết. Bài này gom lại ba thứ: cách tổ chức một project Spring Boot để nó lớn lên mà không rối, những lỗi lặp lại nhiều nhất trong các bài trước, và một checklist ngắn để rà mỗi pull request.

1. Cấu trúc project
Đặt lớp có @SpringBootApplication ở package gốc để component scan bao phủ mọi thứ bên dưới. Chia package theo nghiệp vụ; trong mỗi package, controller chỉ chuyển đổi HTTP, service giữ quy tắc nghiệp vụ và transaction, repository lo truy cập dữ liệu. DTO ở biên API tách khỏi entity, để thay đổi schema không vô tình đổi hợp đồng với client.
vn.motdev.shop
├── ShopApplication.java
├── order
│ ├── OrderController.java // HTTP <-> DTO
│ ├── OrderService.java // rules + @Transactional
│ ├── OrderRepository.java
│ ├── Order.java // entity
│ └── dto/OrderResponse.java
├── payment
└── shared
├── config // @ConfigurationProperties, security
└── error // @RestControllerAdvice, ProblemDetail2. Lỗi hay gặp về bean và transaction
Field injection giấu phụ thuộc và làm lớp khó test; constructor injection với trường final là mặc định nên dùng. @Transactional hoạt động qua proxy, nên nó không có tác dụng khi một phương thức gọi phương thức khác trong cùng lớp, hoặc khi đặt trên phương thức private. Mặc định chỉ RuntimeException mới gây rollback. Giữ transaction ngắn và không gọi API ngoài bên trong nó.
@Service
class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
OrderService(OrderRepository orders, PaymentGateway payments) {
this.orders = orders;
this.payments = payments;
}
@Transactional
public OrderResponse place(PlaceOrder command) {
var order = orders.save(Order.from(command));
return OrderResponse.from(order);
}
}3. Lỗi hay gặp về dữ liệu và API
Trả entity JPA thẳng ra controller kéo theo lazy loading ngoài ý muốn, vòng lặp JSON và lộ trường nội bộ. N+1 truy vấn và danh sách không phân trang chạy tốt với mười hàng dữ liệu test rồi sập ở production. Lỗi nên có một định dạng thống nhất qua @RestControllerAdvice và ProblemDetail. Dữ liệu vào phải được kiểm tra bằng @Valid ngay ở controller.
Tắt spring.jpa.open-in-view và nạp dữ liệu có chủ đích trong service.
Mọi endpoint trả danh sách đều nhận Pageable.
Đặt timeout cho mọi client HTTP và mọi truy vấn.
Không bắt Exception chung rồi im lặng; log một lần và trả lỗi có nghĩa.
Cấu hình qua @ConfigurationProperties có @Validated, không rải @Value.
4. Checklist review
Một checklist ngắn và được dùng thật tốt hơn một tài liệu dài không ai đọc. Dưới đây là những câu hỏi đáng hỏi với mỗi pull request của một service Spring Boot; phần lớn đã có một bài riêng trong series.
Thay đổi có test ở đúng tầng không: unit cho logic, slice hoặc Testcontainers cho tích hợp?
Ranh giới transaction có rõ, và có lời gọi ra ngoài nào nằm bên trong không?
Truy vấn mới có index, có phân trang, có gây N+1 không?
Endpoint mới đã có phân quyền, validation và định dạng lỗi thống nhất chưa?
Có secret, dữ liệu cá nhân hay stack trace nào bị ghi ra log hoặc response không?
Lời gọi ra ngoài có timeout, retry có giới hạn và xử lý trùng lặp chưa?
Thay đổi schema có tương thích ngược với phiên bản đang chạy không?
Có metric hoặc log đủ để biết tính năng này hỏng ở production không?
Thực hành tốt không phải là biết nhiều hơn mà là làm đều những điều cơ bản. Ngày 99 khép lại series bằng một capstone để bạn dùng tất cả cùng lúc.
Tài liệu đối chiếu
https://docs.spring.io/spring-boot/reference/using/structuring-your-code.html
https://docs.spring.io/spring-boot/reference/using/spring-beans-and-dependency-injection.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


