Signals tiện cho cross-cutting hook (audit log, cache invalidation) nhưng lạm dụng khiến code rất khó debug, vì business flow chạy ở đâu đó "ngoài view" mà không ai trace nổi.
Nên bỏ signal khi: logic thuộc cùng app/aggregate với sender (đặt thẳng vào Model.save() override hoặc service function — dễ đọc, dễ test); cần đảm bảo thứ tự hoặc cần biết kết quả (service explicit luôn thắng signal implicit); hoặc khi phải debug câu hỏi "ai vừa update Post mà gây ra email?" — signal chain 3-4 cấp gần như không trace nổi.
Thay bằng service layer thường rõ ràng hơn nhiều:
# Thay vì signal post_save trên Post:
class PostService:
def publish(self, post: Post) -> Post:
with transaction.atomic():
post.status = 'published'
post.save()
self._notify_subscribers(post)
self._invalidate_cache(post)
return postService layer cho test mock-free, log rõ ràng, và caller biết chính xác cái gì xảy ra sau khi gọi.
- Rule thực dụng để quyết: signal chỉ thực sự đáng dùng khi bên gắn listener không thuộc app phát sender (vd app
auditlắng nghepost_savecủa mọi model). - Cùng app thì viết service.
Signals are handy for cross-cutting hooks (audit log, cache invalidation), but overuse makes code hard to debug — business flow runs "somewhere outside the view".
Signs you should drop the signal:
- The logic belongs to the same app/aggregate as the sender → put it directly in a Model.save() override or a service function. Easier to read, easier to test.
- You need ordering guarantees or to know the result → explicit service > implicit signal.
- You have to debug "who just updated a Post that triggered an email?" — 3–4 signal chains are nearly untraceable.
A better replacement:
# Instead of a post_save signal on Post:
class PostService:
def publish(self, post: Post) -> Post:
with transaction.atomic():
post.status = 'published'
post.save()
self._notify_subscribers(post)
self._invalidate_cache(post)
return postThe service layer enables mock-free tests, clear logs, and callers know exactly what happens after the call.
A practical rule of thumb: use a signal only when the listener lives outside the sender's app (e.g. an audit app listening to every post_save). Same app → write a service.