Kiến trúc web app: từ monolith rõ ràng đến hệ thống dễ mở rộng
Kiến trúc tốt bắt đầu từ ranh giới rõ#
Khi nghe đến kiến trúc hệ thống, nhiều người nghĩ ngay tới microservices, event-driven, Kubernetes hoặc message broker. Nhưng với phần lớn sản phẩm web, kiến trúc tốt trước hết là codebase có ranh giới rõ, dữ liệu có chủ sở hữu, workflow dễ hiểu và thay đổi ít gây hiệu ứng dây chuyền. Một monolith được tổ chức tốt thường hiệu quả hơn nhiều microservice được tách sớm nhưng không có lý do vận hành rõ ràng.
Kiến trúc không phải mục tiêu để khoe độ phức tạp. Nó là cách quản lý thay đổi. Khi sản phẩm thêm tính năng, thêm người dùng, thêm developer và thêm dữ liệu, kiến trúc phải giúp team vẫn hiểu được hệ thống và sửa lỗi an toàn.
Monolith không xấu, monolith rối mới xấu#
Monolith có ưu điểm lớn: deploy đơn giản, transaction dễ quản lý, debug thuận tiện và chi phí vận hành thấp. Vấn đề xuất hiện khi mọi module gọi trực tiếp vào mọi bảng, business rule nằm rải rác trong controller, job, command và UI, hoặc một thay đổi nhỏ chạm vào quá nhiều nơi.
Thay vì tách microservice sớm, hãy xây modular monolith. Nghĩa là vẫn deploy một ứng dụng, nhưng code chia theo domain và có boundary rõ.
- Identity: user, role, permission, session.
- Content: post, category, tag, publish state, SEO metadata.
- Commerce: order, payment, invoice, refund nếu có.
- Notification: email, webhook, in-app message.
- Reporting: dashboard, export, analytics query.
Mỗi module nên có model, service, validation và policy riêng. Controller chỉ nên điều phối request/response, không phải nơi chứa toàn bộ business logic.
Layering thực dụng#
Một cấu trúc layer dễ dùng cho web app gồm route/controller, application service, domain logic, data access và infrastructure adapter. Không cần đặt tên quá học thuật, nhưng cần biết logic nào nằm ở đâu.
- Route/controller: parse request, gọi service, trả response.
- Application service: điều phối use case, transaction, permission, event.
- Domain logic: rule cốt lõi như publish bài, tính trạng thái, kiểm tra chuyển state.
- Repository/data access: truy vấn database, mapping dữ liệu.
- Adapter: gửi email, gọi payment gateway, upload storage, gọi API ngoài.
Khi logic gửi email nằm trong adapter, bạn có thể test use case mà không gửi email thật. Khi business rule không nằm trong controller, bạn có thể dùng lại nó cho admin UI, API hoặc background job.
Database là trung tâm của nhiều quyết định#
Trong web app, database thường là tài sản quan trọng nhất. Kiến trúc tốt cần thiết kế schema, index, constraint và migration cẩn thận. Đừng chỉ dựa vào validation ở app. Database nên bảo vệ các bất biến quan trọng như unique slug, foreign key, enum/status hợp lệ, timestamp và constraint số không âm.
Index nên đi theo query thực tế. Nếu trang blog lọc theo published_at, category và slug, hãy index các cột đó. Nếu tìm kiếm full text tăng lên, cân nhắc full-text index hoặc search engine thay vì dùng LIKE tùy tiện trên bảng lớn.
Cache là dao sắc#
Cache giúp giảm latency và tải database, nhưng cache cũng thêm nguy cơ dữ liệu cũ. Trước khi cache, hãy trả lời ba câu hỏi: dữ liệu này có thể cũ bao lâu, cache key là gì, và khi nào invalidate. Nếu không trả lời được, hãy cache ở lớp ít rủi ro hơn như HTTP cache cho public content.
- Browser/CDN cache: tốt cho ảnh, asset, bài viết public ít thay đổi.
- Application cache: tốt cho query đắt nhưng cần invalidate rõ.
- Database cache: tận dụng query plan, index và connection pooling trước khi thêm Redis.
Đừng dùng cache để che schema hoặc query quá tệ. Hãy đo bottleneck trước.
Queue xử lý công việc chậm và dễ retry#
Những việc như gửi email, resize ảnh, gọi webhook, generate report hoặc sync dữ liệu bên ngoài không nên chặn request chính nếu chúng không cần kết quả ngay. Queue giúp request trả nhanh hơn và worker xử lý sau. Nhưng queue cũng đòi hỏi idempotency: job chạy lại không được tạo dữ liệu trùng hoặc gây side effect nguy hiểm.
Ví dụ, job gửi email nên lưu trạng thái đã gửi hoặc dùng idempotency key. Nếu worker chết giữa chừng, hệ thống retry mà không gửi 5 email giống nhau cho người dùng.
Khi nào nên tách service?#
Microservice phù hợp khi boundary domain đã rõ, team đủ năng lực vận hành, service có nhu cầu scale khác nhau, hoặc một phần hệ thống cần độc lập deploy. Tách service chỉ vì codebase lớn thường không giải quyết gốc rễ. Bạn có thể biến một codebase rối thành nhiều service rối, cộng thêm network failure, distributed tracing, versioning API và chi phí deploy.
Dấu hiệu tách service hợp lý gồm: module có database ownership rõ, giao tiếp có contract ổn định, failure của module này không nên kéo sập module khác, và team có khả năng monitor riêng từng service.
Checklist kiến trúc cho web app vừa và nhỏ#
- Module/domain được đặt tên theo nghiệp vụ, không chỉ theo technical layer.
- Controller mỏng, business rule nằm trong service/domain.
- Database có constraint bảo vệ dữ liệu quan trọng.
- Index phản ánh query thật của sản phẩm.
- Background job dùng cho tác vụ chậm hoặc dễ retry.
- Cache có TTL, key và chiến lược invalidate rõ ràng.
- Log và metric được thiết kế theo use case quan trọng.
- API contract được version hoặc quản lý tương thích khi có client bên ngoài.
Kiến trúc tốt là kiến trúc giúp bạn thay đổi có kiểm soát. Hãy bắt đầu bằng modular monolith rõ ràng, đo vấn đề thực tế, rồi chỉ tách phức tạp khi lợi ích vận hành lớn hơn chi phí.
Nguồn tham khảo chính#
Chia sẻ bài viết
Giúp người khác tìm thấy bài viết này
Bình luận
Đang tải bình luận...
Đang tải bình luận...