Chương 15. Service Layer Và Use Case
Ở các chương trước, ta đã có vài mảnh quan trọng:
- Domain: thế giới nghiệp vụ.
- Bounded Context: ranh giới nơi từ ngữ có nghĩa rõ ràng.
- Aggregate: cụm dữ liệu và luật phải đúng cùng nhau.
Nhưng vẫn còn một câu hỏi thực tế:
> Khi người dùng bấm một nút, ai là người điều phối toàn bộ việc cần làm?
Ví dụ khách bấm "Đặt bánh":
- Kiểm tra giỏ hàng.
- Kiểm tra khách có quyền đặt không.
- Tạo đơn.
- Lưu database.
- Phát event.
- Gửi job sang bếp.
- Yêu cầu thanh toán.
- Trả kết quả cho người dùng.
Những việc này không nên nhét hết vào controller.
Cũng không nên nhét hết vào model.
Càng không nên rải lung tung ở nhiều nơi.
Đó là lý do ta cần Service Layer và Use Case.
Nói ngắn gọn:
> Use case là một hành động nghiệp vụ mà hệ thống hỗ trợ.
> Service layer là lớp điều phối để thực thi các use case đó.
---
15.1. Ví dụ quán bánh: từ nút bấm đến nghiệp vụ
Khách vào website bán bánh.
Khách chọn bánh, nhập địa chỉ, bấm:
Đặt hàng
Trong đầu người dùng, đây là một hành động đơn giản.
Nhưng trong hệ thống, có nhiều bước:
1. Nhận request từ frontend.
2. Xác thực người dùng.
3. Lấy giỏ hàng.
4. Kiểm tra giỏ hàng không rỗng.
5. Kiểm tra bánh còn được bán.
6. Tính giá.
7. Tạo Order Aggregate.
8. Lưu order vào database.
9. Ghi event OrderPlaced.
10. Trả order_id cho frontend.
11. Sau đó Payment/Kitchen/Notification xử lý tiếp.
Nếu viết tất cả trong API endpoint, endpoint sẽ rất dài.
Nếu viết tất cả trong model Order, model sẽ biết quá nhiều thứ bên ngoài nó.
Nếu viết rải rác mỗi nơi một chút, sau này sửa sẽ rất mệt.
Cách rõ hơn:
API Controller
-> PlaceOrderUseCase
-> Order Aggregate
-> OrderRepository
-> EventOutbox
Trong đó:
- Controller nhận request và trả response.
- Use case điều phối luồng đặt hàng.
- Aggregate giữ luật của order.
- Repository lưu dữ liệu.
- Outbox ghi event để xử lý sau.
Mỗi thứ làm đúng vai trò.
---
15.2. Use case là gì?
Use case là một việc người dùng hoặc hệ thống muốn hoàn thành.
Ví dụ trong quán bánh:
- Khách đặt hàng.
- Khách hủy đơn.
- Khách thanh toán.
- Admin xác nhận đơn.
- Bếp đánh dấu làm xong.
- Shipper đánh dấu giao thất bại.
- Hệ thống hoàn tiền.
- Hệ thống gửi thông báo.
Use case thường có động từ.
Tên tốt thường giống một hành động:
PlaceOrder
CancelOrder
ConfirmPayment
CreateKitchenTicket
MarkDeliveryFailed
RefundPayment
CompleteGradingJob
SubmitAssignment
Use case không phải là bảng.
Không nên đặt theo kiểu:
OrderService.update()
UserService.handle()
DataService.process()
Tên càng chung, càng khó biết nó làm gì.
Tên tốt giúp ta đọc code như đọc nghiệp vụ.
---
15.3. Service layer là gì?
Service layer là lớp nằm giữa API/controller và domain/model.
Nó chịu trách nhiệm điều phối use case.
Một luồng đơn giản:
HTTP Request
-> Controller
-> Application Service / Use Case
-> Domain Aggregate
-> Repository
-> Database
Nếu cần xử lý bất đồng bộ:
HTTP Request
-> Controller
-> Use Case
-> Save database
-> Save event/job
-> Worker xử lý sau
Service layer thường làm những việc như:
- Kiểm tra quyền ở mức use case.
- Load dữ liệu cần thiết.
- Gọi aggregate method.
- Gọi repository để lưu.
- Quản lý transaction.
- Tạo event.
- Gọi service ngoài khi phù hợp.
- Đẩy job vào queue.
- Chuyển lỗi domain thành lỗi ứng dụng dễ hiểu.
Service layer không nên trở thành nơi nhét mọi logic.
Nó là người điều phối, không phải người làm thay tất cả.
---
15.4. Một phép so sánh dễ hiểu
Hãy tưởng tượng một nhà hàng.
Khách nói với nhân viên:
Tôi muốn đặt một bánh sinh nhật cho ngày mai.
Nhân viên quầy không tự làm bánh, không tự quản lý kho, không tự giao hàng.
Nhưng nhân viên quầy điều phối:
- Nhận yêu cầu.
- Kiểm tra thông tin.
- Tạo phiếu đặt.
- Báo bếp.
- Báo thu ngân.
- Hẹn thời gian nhận.
Trong hệ thống:
- Controller giống người nhận lời từ bên ngoài.
- Use case giống người điều phối quy trình.
- Aggregate giống bộ phận giữ luật nghiệp vụ của một khối.
- Repository giống người cất và lấy hồ sơ.
- Worker giống bộ phận xử lý việc nền.
Nếu nhân viên quầy làm hết mọi việc, sẽ quá tải.
Nếu bếp phải tự nói chuyện với khách, tính tiền, giao hàng, cũng rối.
Mỗi lớp có vai trò riêng.
---
15.5. Vì sao không viết hết trong controller?
Controller là nơi nhận request.
Trong FastAPI, Django, DRF, Laravel, Spring, ASP.NET, Express, controller thường làm việc như:
- Đọc input.
- Validate hình dạng input.
- Gọi use case.
- Trả response.
Controller không nên chứa quá nhiều nghiệp vụ.
Ví dụ controller xấu:
POST /orders
validate request
load cart
check stock
calculate price
create order
create order items
update payment status
send email
call kitchen service
catch many errors
return response
Vấn đề:
- Endpoint dài.
- Khó test nghiệp vụ nếu không đi qua HTTP.
- Worker muốn dùng lại logic cũng khó.
- CLI/admin command muốn dùng lại cũng khó.
- Logic bị dính vào framework.
- Đổi API framework thì động đến nghiệp vụ.
Cách tốt hơn:
Controller:
input = parse_request()
result = place_order_use_case.execute(input)
return response(result)
Nghiệp vụ nằm trong use case.
Controller mỏng hơn.
---
15.6. Vì sao không viết hết trong model?
Nếu theo kiểu Active Record, model thường có cả dữ liệu và method lưu database.
Ví dụ:
order.cancel()
order.save()
Điều này có thể ổn với hệ thống nhỏ.
Nhưng nếu nhét toàn bộ luồng vào model, model sẽ biết quá nhiều.
Ví dụ Order không nên tự:
- Gọi payment gateway.
- Gửi email.
- Đẩy job Celery.
- Gọi AI API.
- Gọi delivery provider.
- Ghi analytics.
Vì những việc này là điều phối bên ngoài aggregate.
Order nên biết luật của order:
- Có được cancel không?
- Có được confirm không?
- Có item hợp lệ không?
- Tổng tiền có đúng không?
Nhưng Order không cần biết cách gửi email hủy đơn.
Cách rõ hơn:
CancelOrderUseCase
order = order_repository.get(order_id)
order.cancel(reason)
order_repository.save(order)
event_outbox.add(OrderCancelled)
Aggregate giữ luật.
Use case điều phối.
---
15.7. Service layer khác aggregate thế nào?
Đây là chỗ rất dễ lẫn.
Aggregate trả lời:
> Bên trong một khối dữ liệu, điều gì luôn phải đúng?
Service/use case trả lời:
> Để hoàn thành một hành động nghiệp vụ, cần phối hợp những bước nào?
Ví dụ Order.cancel(reason):
Aggregate kiểm tra:
- Đơn có tồn tại.
- Đơn ở trạng thái được hủy.
- Lưu lý do hủy.
- Chuyển trạng thái.
CancelOrderUseCase điều phối:
- User có quyền hủy đơn này không?
- Load order từ database.
- Gọi
order.cancel(reason). - Lưu order.
- Nếu đã thanh toán thì yêu cầu refund.
- Ghi event
OrderCancelled. - Trả kết quả cho API.
Aggregate không nên biết user request đến từ HTTP hay worker.
Use case cũng không nên tự set field lung tung bỏ qua aggregate.
Hai bên phối hợp:
Use case điều phối.
Aggregate bảo vệ luật.
---
15.8. Service layer khác repository thế nào?
Repository là nơi lấy và lưu dữ liệu.
Service layer là nơi điều phối hành động.
Repository nên trả lời:
Lấy order này ở đâu?
Lưu order này thế nào?
Tìm payment theo id ra sao?
Service/use case nên trả lời:
Muốn hủy order thì phải làm những bước nào?
Muốn hoàn tiền thì kiểm tra những gì?
Muốn chấm bài AI thì tạo job ra sao?
Ví dụ:
order = order_repository.get(order_id)
order.cancel(reason)
order_repository.save(order)
Repository không quyết định nghiệp vụ hủy đơn.
Nó chỉ giúp lấy/lưu aggregate.
Nếu repository bắt đầu có logic:
cancel_order_and_refund_and_send_email()
thì nó đang vượt vai trò.
---
15.9. Service layer khác domain service thế nào?
Có hai khái niệm dễ nhầm:
- Application service / Use case service.
- Domain service.
Application service điều phối use case.
Domain service chứa logic nghiệp vụ không thuộc tự nhiên về một aggregate cụ thể.
Ví dụ:
PlaceOrderUseCase
là application service.
Nó điều phối:
- Load cart.
- Tạo order.
- Lưu order.
- Ghi event.
Còn:
PricingService
có thể là domain service nếu nó tính giá theo luật nghiệp vụ phức tạp:
- Giá theo khung giờ.
- Coupon.
- Hạng khách hàng.
- Phí giao hàng.
- Thuế.
Nếu logic tính giá không thuộc hẳn về Cart hay Order, nó có thể nằm trong domain service.
Nhưng domain service không nên gọi database lung tung hoặc xử lý HTTP.
Nó nên là logic nghiệp vụ thuần nhất có thể.
---
15.10. Một use case tốt trông như thế nào?
Một use case tốt thường có vài đặc điểm:
- Tên rõ hành động.
- Input rõ.
- Output rõ.
- Điều phối một nghiệp vụ cụ thể.
- Không quá chung.
- Không phụ thuộc quá sâu vào framework.
- Có thể test mà không cần gọi HTTP thật.
- Biết dùng aggregate, repository, event, queue đúng vai trò.
Ví dụ:
PlaceOrderUseCase
Input:
customer_id
cart_id
delivery_address
payment_method
Output:
order_id
status
next_action
Các bước:
1. Load cart.
2. Kiểm tra cart có item.
3. Kiểm tra sản phẩm còn bán.
4. Tính giá.
5. Tạo Order.
6. Lưu Order.
7. Ghi OrderPlaced event.
8. Trả order_id.
Đọc vào là hiểu hệ thống đang làm gì.
Đó là mục tiêu.
---
15.11. Use case không nên quá nhỏ
Một lỗi là biến mỗi hàm nhỏ thành một use case.
Ví dụ:
ValidateCartUseCase
CalculatePriceUseCase
CreateOrderUseCase
SaveOrderUseCase
SendOrderEventUseCase
Nếu tách như vậy mà không có lý do, luồng đặt hàng bị vỡ thành quá nhiều mảnh.
Use case nên đại diện cho một mục tiêu nghiệp vụ có nghĩa.
Ví dụ:
PlaceOrderUseCase
CancelOrderUseCase
RefundPaymentUseCase
SubmitAssignmentUseCase
CompleteGradingJobUseCase
Bên trong use case có thể gọi helper, domain service, aggregate method.
Nhưng đừng biến từng dòng code thành use case.
---
15.12. Use case không nên quá to
Lỗi ngược lại là có một service khổng lồ:
OrderService
create_order()
cancel_order()
pay_order()
refund_order()
assign_shipper()
send_email()
export_report()
update_analytics()
handle_complaint()
sync_accounting()
Ban đầu tiện.
Sau đó OrderService thành "god service".
Dấu hiệu:
- File rất dài.
- Method không liên quan chặt với nhau.
- Service phụ thuộc vào quá nhiều repository/client.
- Sửa một use case phải sợ ảnh hưởng use case khác.
- Test phải mock rất nhiều thứ.
- Không ai biết service này thật sự chịu trách nhiệm gì.
Cách tốt hơn:
PlaceOrderUseCase
CancelOrderUseCase
ConfirmPaymentUseCase
CreateDeliveryTaskUseCase
RefundPaymentUseCase
Hoặc nhóm theo module nhưng use case vẫn rõ.
Không nhất thiết mỗi use case là một file.
Nhưng trong đầu và trong code, ranh giới hành động phải rõ.
---
15.13. Service layer giúp code dễ đổi kiến trúc hơn thế nào?
Giả sử ban đầu hệ thống là monolith.
Controller gọi use case:
POST /orders
-> PlaceOrderUseCase
Sau này, bạn muốn:
- Chuyển một phần sang worker.
- Đổi từ DRF sang FastAPI.
- Tách Payment thành service riêng.
- Thêm API mobile.
- Thêm admin command.
- Thêm webhook từ payment gateway.
Nếu nghiệp vụ nằm trong controller, mỗi thay đổi sẽ đụng vào HTTP layer.
Nếu nghiệp vụ nằm trong use case, nhiều cổng vào khác nhau có thể dùng chung:
HTTP API
CLI command
Admin action
Celery worker
Webhook handler
Scheduled job
cùng gọi vào use case phù hợp.
Ví dụ:
PaymentWebhookController
-> ConfirmPaymentUseCase
PaymentRetryWorker
-> RetryPaymentUseCase
AdminRefundAction
-> RefundPaymentUseCase
Đây là lợi ích lớn:
> Use case giúp nghiệp vụ bớt dính vào cách hệ thống được gọi.
Khi kiến trúc thay đổi, nghiệp vụ ít bị xáo trộn hơn.
---
15.14. Ví dụ với DRF, Celery và FastAPI
Trong cuộc trò chuyện trước, ta có nói đến DRF, Celery, FastAPI, AI Judge.
Một lỗi thường gặp là nghĩ:
> Nếu endpoint DRF chậm, chuyển logic sang FastAPI là xong.
Không hẳn.
Vấn đề thật thường là:
- Luồng nghiệp vụ đang nằm sai chỗ.
- Request đồng bộ đang ôm việc quá lâu.
- Worker concurrency thấp.
- Gọi AI API là I/O lâu.
- Chưa có queue/job rõ.
- Chưa có trạng thái job rõ.
Use case giúp ta tách vấn đề:
DRF API:
nhận request nộp bài
gọi SubmitAssignmentUseCase
trả submission_id
SubmitAssignmentUseCase:
lưu submission
tạo GradingJob
đẩy job vào queue
Celery Worker:
nhận grading_job_id
gọi RunGradingJobUseCase
RunGradingJobUseCase:
load GradingJob
gọi AI Judge
lưu kết quả
phát GradingCompleted
Nếu sau này muốn dùng FastAPI cho AI Judge service:
RunGradingJobUseCase
-> gọi AI Judge service qua HTTP
Hoặc nếu muốn worker gọi Gemini trực tiếp:
RunGradingJobUseCase
-> gọi Gemini API
Điểm quan trọng:
> DRF, FastAPI, Celery là cách chạy. Use case là nghiệp vụ.
Khi use case rõ, ta dễ đổi cách chạy hơn.
---
15.15. Service layer và transaction
Service layer thường là nơi mở transaction.
Ví dụ:
PlaceOrderUseCase
begin transaction
load cart
create order aggregate
save order
save outbox event
commit
Tại sao không để controller mở transaction?
Vì controller chỉ là cổng vào.
Cùng use case có thể được gọi từ HTTP, worker, CLI.
Transaction thuộc về use case, vì use case biết hành động nghiệp vụ cần nhất quán ở đâu.
Tại sao không để aggregate mở transaction?
Vì aggregate không nên biết database.
Aggregate chỉ biết luật.
Use case biết khi nào load, lưu, commit.
Một quy tắc thực dụng:
> Transaction boundary thường nằm quanh một use case hoặc một phần quan trọng của use case.
Nhưng đừng giữ transaction trong khi gọi API ngoài lâu.
Ví dụ không nên:
begin transaction
save order
call payment gateway
call email provider
commit
Cách tốt hơn thường là:
begin transaction
save order
save outbox event
commit
worker/payment flow xử lý tiếp
---
15.16. Service layer và queue
Use case không nhất thiết chạy toàn bộ ngay trong request.
Nó có thể tạo job.
Ví dụ chấm bài AI mất 1 phút 30 giây.
Không nên để người dùng chờ HTTP request 90 giây nếu không cần.
Luồng tốt hơn:
SubmitAssignmentUseCase
lưu bài nộp
tạo GradingJob = PENDING
đẩy job vào queue
trả về submission_id/job_id
RunGradingJobUseCase
worker lấy job
chuyển job = RUNNING
gọi Gemini API
lưu kết quả
chuyển job = SUCCEEDED
phát GradingCompleted
Ở đây có hai use case:
- Một use case cho hành động nộp bài.
- Một use case cho hành động chạy job chấm.
Controller không cần biết chi tiết gọi AI.
Worker không cần tự viết lại logic domain.
Queue chỉ là phương tiện chuyển việc.
Use case vẫn là nơi mô tả việc cần làm.
---
15.17. Service layer và event
Khi một use case hoàn thành một thay đổi quan trọng, nó có thể ghi event.
Ví dụ:
PlaceOrderUseCase
-> OrderPlaced
ConfirmPaymentUseCase
-> PaymentSucceeded
CompleteGradingJobUseCase
-> GradingCompleted
Event giúp các phần khác phản ứng mà không cần use case gọi trực tiếp tất cả.
Ví dụ sau khi GradingCompleted:
- Learning cập nhật tiến độ.
- Notification gửi thông báo.
- Analytics ghi chi phí.
CompleteGradingJobUseCase không nên tự làm hết mọi thứ.
Nó chỉ cần hoàn thành việc chấm và công bố sự kiện.
Các use case khác hoặc worker khác xử lý phần của mình.
Đây là cách tránh service layer thành một cái dây chuyền khổng lồ.
---
15.18. Service layer và API bên ngoài
Use case đôi khi cần gọi API bên ngoài:
- Payment gateway.
- Gemini/OpenAI API.
- Email provider.
- SMS provider.
- Delivery provider.
Câu hỏi là:
> Gọi trực tiếp trong use case hay đẩy sang worker?
Tùy trường hợp.
Có thể gọi trực tiếp khi:
- Thời gian ngắn.
- User cần kết quả ngay.
- Có timeout rõ.
- Có retry/idempotency.
- Không giữ transaction khi gọi.
Nên đẩy sang worker khi:
- Thời gian lâu.
- Có thể retry sau.
- Không cần user chờ.
- Gọi nhiều API.
- Dễ timeout.
- Cần kiểm soát concurrency.
Ví dụ AI Judge 90 giây:
Nên dùng job nền.
Ví dụ kiểm tra mã giảm giá nội bộ:
Có thể xử lý ngay.
Ví dụ thanh toán:
Tùy mô hình.
Có thể yêu cầu payment ngay để lấy redirect URL, nhưng xác nhận cuối cùng nên qua webhook/use case riêng.
---
15.19. Use case và idempotency
Use case trong hệ thống thật thường bị gọi lại.
Ví dụ:
- User bấm nút hai lần.
- Frontend retry.
- Celery retry.
- Webhook gửi lại.
- Message queue deliver lại.
Vì vậy use case quan trọng nên nghĩ về idempotency.
Ví dụ:
SubmitAssignmentUseCase
Cần hỏi:
- Nếu request gửi lại, có tạo hai submission không?
- Có tạo hai grading job không?
- Có trừ quota hai lần không?
ConfirmPaymentUseCase
Cần hỏi:
- Nếu payment webhook gửi lại, có ghi nhận thanh toán hai lần không?
- Có phát
PaymentSucceededhai lần gây side effect không?
CompleteGradingJobUseCase
Cần hỏi:
- Nếu worker retry sau timeout, có ghi hai kết quả không?
- Nếu job đã succeeded rồi, retry xử lý thế nào?
Idempotency không phải chi tiết phụ.
Nó là một phần của thiết kế use case trong hệ thống phân tán.
---
15.20. Use case và quyền truy cập
Quyền truy cập có nhiều tầng.
Controller có thể kiểm tra:
- User đã đăng nhập chưa?
- Token hợp lệ không?
Nhưng use case nên kiểm tra quyền nghiệp vụ:
- User này có được hủy đơn này không?
- Teacher này có được chấm bài lớp này không?
- Admin này có được hoàn tiền đơn này không?
- Shipper này có được cập nhật delivery task này không?
Ví dụ:
CancelOrderUseCase
Input:
actor_id
order_id
reason
Logic:
load order
check actor can cancel this order
order.cancel(reason)
save
Nếu chỉ kiểm tra quyền ở controller, worker hoặc admin command có thể bỏ qua.
Use case là nơi tốt để đặt authorization theo nghiệp vụ.
---
15.21. Use case và validation
Validation cũng có nhiều tầng.
Controller kiểm tra hình dạng request:
- Field có tồn tại không?
- Kiểu dữ liệu đúng không?
- Chuỗi có quá dài không?
Use case kiểm tra điều kiện ứng dụng:
- Cart có tồn tại không?
- User có quyền không?
- Assignment còn mở không?
- Job có thể retry không?
Aggregate kiểm tra invariant:
- Order không được rỗng khi confirm.
- Wallet không được âm.
- GradingJob cancelled không được complete.
Nếu đặt mọi validation ở controller, logic bị dính vào HTTP.
Nếu đặt mọi validation ở aggregate, aggregate phải biết quá nhiều context bên ngoài.
Chia đúng tầng giúp code dễ hiểu hơn.
---
15.22. Service layer và lỗi
Use case nên chuyển lỗi kỹ thuật thành lỗi nghiệp vụ có nghĩa.
Ví dụ:
Thay vì để controller nhận lỗi mơ hồ:
DatabaseIntegrityError
TimeoutError
KeyError
Use case có thể trả lỗi rõ hơn:
OrderNotFound
OrderCannotBeCancelled
PaymentAlreadyProcessed
GradingJobAlreadyCompleted
QuotaExceeded
Controller chỉ cần map lỗi sang response:
OrderNotFound -> 404
OrderCannotBeCancelled -> 409
QuotaExceeded -> 403 hoặc 429 tùy nghiệp vụ
Việc này làm API rõ hơn và test dễ hơn.
---
15.23. Service layer và test
Một lợi ích rất lớn của service layer là test.
Nếu nghiệp vụ nằm trong controller, muốn test phải dựng HTTP request.
Nếu nghiệp vụ nằm trong use case, có thể test trực tiếp:
PlaceOrderUseCase
given cart has items
when execute
then order is created and OrderPlaced event is recorded
Test use case thường kiểm tra:
- Input nào hợp lệ.
- Input nào bị từ chối.
- Aggregate được thay đổi đúng không.
- Repository được gọi đúng không.
- Event/job được tạo đúng không.
- Lỗi được trả đúng không.
Không cần mock cả web framework.
Điều này rất quan trọng khi hệ thống lớn.
---
15.24. Use case trong monolith
Trong monolith, use case có thể nằm trong module.
Ví dụ:
ordering/
application/
place_order.py
cancel_order.py
domain/
order.py
infrastructure/
order_repository.py
api/
order_controller.py
Không cần quá hình thức.
Nhưng cần rõ:
- API nằm đâu.
- Use case nằm đâu.
- Domain nằm đâu.
- Repository nằm đâu.
Modular monolith mạnh lên rất nhiều khi có use case rõ.
Vì sau này nếu tách service, ta biết phần nghiệp vụ chính nằm ở đâu.
---
15.25. Use case trong microservices
Trong microservices, mỗi service vẫn nên có service layer/use case riêng.
Ví dụ:
payment-service
ConfirmPaymentUseCase
RefundPaymentUseCase
HandlePaymentWebhookUseCase
Microservice không có nghĩa là controller gọi database trực tiếp cho nhanh.
Mỗi service vẫn có nghiệp vụ riêng.
Service layer giúp:
- Giữ logic trong service rõ.
- Dễ test.
- Dễ xử lý event/webhook/worker chung một cách.
- Dễ thay đổi API bên ngoài mà không phá domain.
Nếu microservice chỉ là CRUD mỏng quanh database, nó có thể trở thành distributed CRUD, không phải service nghiệp vụ thật.
---
15.26. Use case và worker
Worker không nên là nơi chứa logic lộn xộn.
Worker nên là một cổng vào khác, giống controller.
Ví dụ Celery task:
run_grading_job_task(job_id)
RunGradingJobUseCase.execute(job_id)
Task không nên chứa toàn bộ logic:
load submission
build prompt
call AI
parse result
update score
send email
update progress
...
Vì nếu sau này muốn chạy lại từ admin, chạy bằng CLI, hoặc chuyển sang queue khác, logic bị kẹt trong Celery.
Cách nghĩ tốt:
HTTP controller là adapter.
Celery task là adapter.
Webhook handler là adapter.
CLI command là adapter.
Use case là nghiệp vụ.
Adapter có thể đổi.
Use case nên ổn định hơn.
---
15.27. Use case và orchestration
Một use case đôi khi chỉ điều phối một aggregate.
Ví dụ:
CancelOrderUseCase
Nhưng đôi khi use case điều phối nhiều bước.
Ví dụ:
CheckoutUseCase
có thể:
- Tạo order.
- Giữ tồn kho.
- Khởi tạo payment.
- Ghi event.
Nếu luồng ngắn và trong cùng service, use case điều phối trực tiếp được.
Nếu luồng dài, qua nhiều service, dễ lỗi, cần retry/compensation, ta nên nghĩ đến saga/workflow.
Không nên để một use case gọi 10 service khác nhau một cách đồng bộ rồi hy vọng mọi thứ ổn.
Khi use case bắt đầu giống một quy trình nhiều bước dài, hãy hỏi:
> Đây có còn là một transaction ứng dụng ngắn không, hay đã là một workflow?
---
15.28. Service layer khác god service ở đâu?
Service layer tốt:
- Có use case rõ.
- Method có tên nghiệp vụ.
- Phụ thuộc vừa đủ.
- Điều phối, không ôm hết logic.
- Gọi aggregate/domain service để xử lý luật.
- Ghi event/job thay vì gọi mọi thứ trực tiếp.
- Dễ test từng hành động.
God service:
- Tên quá chung.
- Làm quá nhiều việc.
- Biết quá nhiều hệ thống bên ngoài.
- Có quá nhiều method không liên quan.
- Logic nghiệp vụ lẫn logic hạ tầng.
- Dễ bị mọi người nhét thêm code.
- Sửa một chỗ sợ vỡ nhiều chỗ.
Ví dụ god service:
OrderService
place_order
cancel_order
refund
send_email
export_excel
assign_shipper
calculate_monthly_revenue
sync_crm
Cách chia tốt hơn:
Ordering:
PlaceOrderUseCase
CancelOrderUseCase
Payment:
RefundPaymentUseCase
Delivery:
AssignDeliveryTaskUseCase
Reporting:
ExportOrderReportUseCase
Integration:
SyncCrmCustomerUseCase
Không phải cứ có chữ Service là xấu.
Xấu là khi service mất ranh giới.
---
15.29. Tên gọi: service, use case, command handler
Trong các codebase khác nhau, cùng ý tưởng có thể có tên khác:
- Application Service.
- Use Case.
- Command Handler.
- Interactor.
- Action.
- Operation.
Tên không quan trọng bằng vai trò.
Nếu một class/function:
- Nhận input của một hành động.
- Load dữ liệu cần thiết.
- Gọi domain logic.
- Lưu thay đổi.
- Phát event/job.
- Trả output.
thì nó đang đóng vai trò use case/application service.
Đừng bị kẹt vào tên gọi.
Hãy nhìn trách nhiệm.
---
15.30. Use case có nên trả entity không?
Một câu hỏi thực tế:
> Use case nên trả về domain object, hay DTO/result?
Thường nên trả về một result rõ cho nhu cầu bên ngoài.
Ví dụ:
PlaceOrderResult
- order_id
- status
- payment_url
Không nhất thiết trả toàn bộ Order.
Vì controller không cần biết mọi chi tiết nội bộ của aggregate.
Nếu trả domain object ra ngoài quá nhiều, API layer dễ phụ thuộc vào model nội bộ.
Tuy nhiên, với hệ thống nhỏ, trả object cũng có thể chấp nhận nếu team kiểm soát được.
Quy tắc thực dụng:
> Use case nên trả đúng thứ caller cần, không phơi toàn bộ ruột domain nếu không cần.
---
15.31. Use case input nên thiết kế thế nào?
Input của use case nên rõ ràng.
Ví dụ:
PlaceOrderInput
- actor_id
- cart_id
- delivery_address
- payment_method
- idempotency_key
Không nên để use case tự đọc request HTTP.
Không nên truyền nguyên request object của framework vào sâu trong application layer.
Ví dụ không tốt:
PlaceOrderUseCase.execute(request)
Vì như vậy use case phụ thuộc vào framework.
Cách tốt hơn:
input = PlaceOrderInput(...)
place_order.execute(input)
Với codebase nhỏ, có thể dùng dict/object đơn giản.
Nhưng ý tưởng vẫn là:
> Use case nhận dữ liệu nghiệp vụ, không nhận object hạ tầng nếu không cần.
---
15.32. Use case và dependency injection
Use case thường cần một số dependency:
- Repository.
- Unit of Work/transaction manager.
- Event publisher/outbox.
- External client.
- Clock.
- Id generator.
- Permission checker.
Ví dụ:
PlaceOrderUseCase(
order_repository,
cart_repository,
pricing_service,
event_outbox,
transaction_manager
)
Đừng để use case tự tạo mọi dependency bên trong.
Nếu nó tự tạo:
db = Database()
email = EmailClient()
thì test khó và thay đổi khó.
Dependency injection không cần quá phức tạp.
Nó chỉ có nghĩa:
> Những thứ use case cần được đưa từ ngoài vào, để có thể thay thế khi test hoặc khi đổi hạ tầng.
---
15.33. Unit of Work là gì?
Unit of Work là cách gom nhiều thao tác lưu dữ liệu trong một transaction.
Nói dễ hiểu:
> Unit of Work là người giữ cuốn sổ: trong use case này, ta đã thay đổi những gì, và khi nào commit.
Ví dụ:
with unit_of_work:
order = order_repository.get(order_id)
order.cancel(reason)
order_repository.save(order)
event_outbox.add(OrderCancelled)
unit_of_work.commit()
Unit of Work giúp:
- Quản lý transaction.
- Gom repository dùng chung session/connection.
- Commit/rollback rõ.
- Test dễ hơn.
Không phải dự án nào cũng cần pattern này một cách hình thức.
Nhưng ý tưởng transaction boundary quanh use case là rất quan trọng.
---
15.34. Use case và CQRS nhẹ
CQRS nghĩa là tách command và query.
Nghe lớn, nhưng dùng nhẹ rất đơn giản:
- Use case ghi dữ liệu:
PlaceOrder,CancelOrder,CompleteGradingJob. - Query đọc dữ liệu:
GetOrderDetail,ListOrders,GetGradingResult.
Không nhất thiết phải tách thành hệ thống phức tạp.
Chỉ cần hiểu:
> Đọc và ghi có nhu cầu khác nhau.
Ghi cần:
- Aggregate.
- Invariant.
- Transaction.
- Event.
Đọc cần:
- Nhanh.
- Đúng format màn hình.
- Có filter/sort/search.
- Có thể dùng read model.
Nếu ép mọi truy vấn đọc đi qua aggregate, hệ thống nặng.
Nếu ép mọi ghi chỉ là update field, luật nghiệp vụ yếu.
Tách nhẹ command/query giúp code sáng hơn.
---
15.35. Ví dụ hoàn chỉnh: PlaceOrderUseCase
Ta viết bằng pseudo-code để thấy vai trò, không cần quan tâm cú pháp.
PlaceOrderUseCase.execute(input):
actor = auth_service.get_actor(input.actor_id)
cart = cart_repository.get(input.cart_id)
if cart.owner_id != actor.id:
raise PermissionDenied
price = pricing_service.calculate(cart.items, input.delivery_address)
order = Order.place(
customer_id = actor.id,
items = cart.items,
price = price,
delivery_address = input.delivery_address
)
begin transaction
order_repository.save(order)
cart.mark_checked_out()
cart_repository.save(cart)
outbox.add(OrderPlaced(order.id))
commit
return PlaceOrderResult(order_id = order.id)
Trong đó:
- Auth service kiểm tra actor.
- Cart repository lấy cart.
- Pricing service tính giá.
- Order aggregate tạo order hợp lệ.
- Repository lưu.
- Outbox ghi event.
- Use case điều phối toàn bộ.
Nghiệp vụ không nằm trong controller.
Database không nằm trong aggregate.
Email không nằm trong transaction.
Mọi thứ vừa đủ rõ.
---
15.36. Ví dụ hoàn chỉnh: RunGradingJobUseCase
Với AI Judge:
RunGradingJobUseCase.execute(job_id):
job = grading_job_repository.get(job_id)
job.start()
grading_job_repository.save(job)
submission = submission_repository.get(job.submission_id)
rubric = rubric_repository.get(job.rubric_id)
try:
result = ai_judge_client.grade(submission.content, rubric)
job.complete(result.score, result.feedback)
except Timeout:
job.record_failure("AI timeout")
if job.can_retry():
queue.enqueue(job.id)
except AiProviderError as error:
job.record_failure(error.message)
begin transaction
grading_job_repository.save(job)
outbox.add_events(job.events)
commit
Ở đây cần chú ý:
GradingJobgiữ luật trạng thái.- Use case điều phối gọi AI.
- Worker chỉ gọi use case.
- Retry phải idempotent.
- Không nên để HTTP request của học viên chờ toàn bộ.
Nếu Gemini API mất 1 phút 30 giây cho một bài, đây là use case nên chạy trong worker, không phải trong request chính.
---
15.37. Khi nào chưa cần service layer quá rõ?
Không phải dự án nhỏ nào cũng cần đầy đủ pattern.
Nếu chỉ là CRUD admin đơn giản:
- Tạo category.
- Sửa banner.
- Đổi mô tả.
- Bật/tắt tag.
Controller gọi model/repository trực tiếp có thể chấp nhận.
Nhưng nên bắt đầu tạo use case khi:
- Một endpoint có nhiều bước nghiệp vụ.
- Logic bị copy ở nhiều nơi.
- Worker và API cần dùng chung logic.
- Có transaction quan trọng.
- Có event/queue.
- Có quyền nghiệp vụ phức tạp.
- Có gọi API bên ngoài.
- Có retry/idempotency.
- Có kế hoạch tách module/service.
Không cần làm nặng từ đầu.
Nhưng khi nghiệp vụ bắt đầu thật, service layer giúp hệ thống không bị loang.
---
15.38. Dấu hiệu bạn đang cần use case
Bạn có thể cần use case rõ hơn nếu thấy:
- Controller dài hơn vài chục dòng nghiệp vụ.
- Celery task chứa nhiều logic copy từ API.
- Cùng một hành động được làm ở API và admin nhưng khác logic.
- Model có method gọi email/payment/API ngoài.
- Service tên quá chung và ngày càng phình.
- Test nghiệp vụ phải dựng HTTP request.
- Mỗi lần thêm worker là copy logic từ view/controller.
- Không rõ transaction nằm ở đâu.
- Không rõ event được phát khi nào.
- Không rõ lỗi nghiệp vụ nằm ở đâu.
Những dấu hiệu này không có nghĩa hệ thống tệ.
Chúng chỉ báo rằng nghiệp vụ đã đủ lớn để cần một lớp điều phối rõ.
---
15.39. Bảng phân vai nhanh
| Thành phần | Vai trò chính | Không nên làm | |---|---|---| | Controller/API | Nhận request, gọi use case, trả response | Ôm nghiệp vụ dài | | Use case/Application service | Điều phối một hành động nghiệp vụ | Trở thành god service | | Aggregate | Giữ invariant và luật cục bộ | Gọi API ngoài, biết framework | | Domain service | Logic domain không thuộc một aggregate | Quản lý HTTP/database lộn xộn | | Repository | Load/save aggregate | Quyết định nghiệp vụ | | Worker | Chạy việc nền, gọi use case | Chứa logic riêng copy từ API | | Event handler | Phản ứng với event | Làm quá nhiều việc không liên quan | | Read model/query | Phục vụ đọc nhanh | Thay thế luật ghi |
---
15.40. Một cấu trúc thư mục thực dụng
Không có cấu trúc duy nhất đúng.
Nhưng với modular monolith, có thể tham khảo:
ordering/
api/
order_controller
application/
place_order
cancel_order
get_order_detail
domain/
order
order_item
pricing_policy
infrastructure/
order_repository
outbox_repository
Với AI Grading:
grading/
api/
grading_controller
workers/
run_grading_job_task
application/
submit_assignment
run_grading_job
complete_grading_job
domain/
grading_job
rubric
infrastructure/
grading_job_repository
ai_judge_client
Điểm chính không phải tên thư mục.
Điểm chính là dependency đi đúng chiều:
API/Worker
-> Application
-> Domain
-> Repository interface
-> Infrastructure implementation
Domain không nên phụ thuộc ngược vào API framework.
---
15.41. Cách refactor từ code đang rối
Nếu hiện tại logic đang nằm trong controller hoặc worker, không cần đập đi làm lại.
Có thể refactor từng bước.
Bước 1: Chọn một endpoint đau nhất
Ví dụ:
POST /submissions
hoặc:
POST /orders
Bước 2: Đặt tên use case
Ví dụ:
SubmitAssignmentUseCase
PlaceOrderUseCase
Bước 3: Chuyển logic điều phối vào use case
Controller chỉ còn parse input và gọi use case.
Bước 4: Tách luật cốt lõi vào aggregate/domain
Nếu thấy use case đang tự set status lung tung, chuyển vào method domain.
Bước 5: Tách gọi ngoài sang client/adapter
Ví dụ:
- AiJudgeClient.
- EmailClient.
- PaymentGatewayClient.
Bước 6: Thêm test cho use case
Test hành vi chính và lỗi quan trọng.
Bước 7: Lặp lại với endpoint tiếp theo
Không cần refactor toàn bộ hệ thống trong một lần.
---
15.42. Những lỗi phổ biến
Lỗi 1: Service chỉ là wrapper vô nghĩa
Ví dụ:
OrderService.create(data):
return Order.objects.create(data)
Nếu service không thêm ý nghĩa gì, nó chỉ làm code dài hơn.
Service layer có giá trị khi nó điều phối use case hoặc giữ ranh giới nghiệp vụ.
Lỗi 2: Service thành god object
Một service làm mọi thứ.
Đây là lỗi ngược lại.
Lỗi 3: Use case phụ thuộc thẳng vào framework
Ví dụ use case nhận request, trả Response.
Khi đó nó không còn độc lập với API layer.
Lỗi 4: Aggregate bị bỏ qua
Use case tự sửa field:
order.status = "CANCELLED"
thay vì:
order.cancel(reason)
Như vậy luật nghiệp vụ dễ bị rải rác.
Lỗi 5: Transaction quá rộng
Use case mở transaction rồi gọi API ngoài, gửi email, chạy xử lý lâu.
Dễ gây lock và lỗi khó sửa.
Lỗi 6: Worker chứa nghiệp vụ riêng
API có một logic, worker có một logic gần giống.
Sau vài tháng, hai bên lệch nhau.
Lỗi 7: Không thiết kế output
Use case trả lung tung object nội bộ, khiến API phụ thuộc vào domain quá sâu.
---
15.43. Bài tập: đọc một endpoint
Lấy một endpoint trong hệ thống hiện tại.
Ví dụ:
POST /orders
POST /submissions
POST /payments/webhook
POST /grading-jobs/{id}/retry
Hỏi:
1. Đây là use case gì? 2. Tên use case có thể là gì? 3. Input nghiệp vụ gồm những gì? 4. Output cần trả là gì? 5. Controller đang làm quá nhiều không? 6. Có aggregate nào nên giữ luật không? 7. Transaction nằm ở đâu? 8. Có gọi API ngoài không? 9. Có cần queue không? 10. Có cần idempotency không? 11. Worker/admin/webhook có cần dùng lại logic này không? 12. Lỗi nghiệp vụ nào có thể xảy ra?
Trả lời xong, bạn sẽ thấy rõ service layer nên nằm ở đâu.
---
15.44. Tóm tắt bằng một luồng
Một luồng đặt bánh tốt có thể nhìn như sau:
Frontend
-> OrderController
-> PlaceOrderUseCase
-> CartRepository
-> PricingService
-> Order Aggregate
-> OrderRepository
-> Outbox
-> OrderPlaced event
-> Payment/Kitchen/Notification xử lý sau
Mỗi phần có vai trò:
- Frontend gửi ý định.
- Controller chuyển request thành input.
- Use case điều phối.
- Aggregate giữ luật.
- Repository lưu.
- Outbox đảm bảo event không mất.
- Worker/service khác xử lý việc tiếp theo.
Đây là cách hệ thống vừa rõ nghiệp vụ vừa dễ đổi kiến trúc.
---
15.45. Kết luận của chương
Service layer và use case giúp ta đặt nghiệp vụ vào một nơi dễ hiểu, dễ test, dễ thay đổi.
Chúng trả lời câu hỏi:
> Khi hệ thống cần thực hiện một hành động nghiệp vụ, ai điều phối các bước?
Controller không nên ôm nghiệp vụ.
Aggregate không nên biết hạ tầng bên ngoài.
Repository không nên quyết định nghiệp vụ.
Worker không nên copy logic riêng.
Use case là nơi kết nối các phần lại:
- Nhận input nghiệp vụ.
- Kiểm tra quyền và điều kiện.
- Load aggregate.
- Gọi domain logic.
- Lưu thay đổi.
- Quản lý transaction.
- Ghi event/job.
- Trả result rõ ràng.
Thông điệp quan trọng nhất:
> Use case là câu chuyện nghiệp vụ được viết thành code. Service layer là nơi câu chuyện đó được điều phối một cách có trật tự.
Khi thiết kế tốt service layer, ta không chỉ làm code sạch hơn.
Ta làm hệ thống dễ tiến hóa hơn: từ monolith sang modular monolith, từ request đồng bộ sang queue, từ worker nội bộ sang service riêng, mà nghiệp vụ cốt lõi vẫn không bị vỡ vụn.