Django Signals: Decoupling Application Logic Without Hiding It
Learn Django signals, common use cases, risks, testing, transaction safety, alternatives, and practical rules for maintainable event-like behavior.
Signals let code react to events
Django signals allow one part of an application to react when something happens elsewhere. Common examples include running code after a model is saved, after a user logs in, or before an object is deleted. This can be useful for audit logs, cache invalidation, notifications, search indexing, and lightweight side effects that should not clutter the main code path.
The danger is invisibility. A developer may save a model and not realize that several signal handlers also run. Hidden behavior can create performance problems, unexpected database writes, duplicate emails, or tests that fail for reasons far from the code being changed. Signals should be used carefully and named clearly.
Use signals for true cross-cutting reactions
Signals fit best when the sender should not need to know every receiver. For example, an audit system may listen to changes without every view calling audit code manually. A cache invalidation handler may react to model updates. But core business workflows often deserve explicit service calls because the order, error handling, and intent are important.
If placing an order must charge a card, update inventory, send email, and create a shipment, hiding that workflow in signals can make the system hard to reason about. A service or task orchestration layer may be clearer.
- Keep signal handlers small and focused.
- Avoid slow network calls directly inside request-time signals.
- Use transaction hooks when work should happen only after commit.
- Test signal behavior explicitly when it affects important outcomes.
Transactions matter
A common signal mistake is doing side effects before the database transaction commits. If a handler sends an email or publishes a message and the transaction later rolls back, external systems may believe something happened when it did not. Use transaction.on_commit when a side effect should occur only after the database change is durable.
Also think about retries. If a signal handler fails, should the original save fail? Should the side effect be retried? Should work move to a background queue? The answer depends on the business importance of the reaction.
Make hidden behavior visible
Signals are easier to maintain when handlers live in predictable files, have clear names, and are registered in a conventional way. Document important signals and avoid scattering handlers across unrelated modules. During debugging, developers should be able to find what reacts to a model change quickly.
Django signals are useful, but they are not a replacement for architecture. Use them when decoupling genuinely helps, and prefer explicit calls when the workflow needs visibility, ordering, and direct error handling.
Keep registration predictable
Signal handlers should be imported reliably when Django starts. If handlers are hidden in modules that may not load, behavior can differ between tests, development, and production. Put registration in a conventional place and keep names descriptive. Predictable registration makes signal behavior easier to trust and debug.