← Blog

Why I Choose django-lifecycle Over Django Signals

In Django projects, signals.py tends to appear quickly. It feels convenient. Save a model, trigger an email. Create an object, write an audit log. Change a status, run side effects.

The problem is that convenience often comes with weaker visibility.

What Goes Wrong with Signals

Signals can keep working long after they stop being clear.

  • saving a model no longer tells you what side effects will happen
  • multiple handlers attach to the same event over time
  • tests become more indirect
  • domain rules drift into scattered event listeners

Eventually save() becomes harder to reason about than it should be.

Why django-lifecycle Fits Me Better

django-lifecycle keeps lifecycle rules close to the model. The key benefit is not magic. It is readability.

  • field-change conditions are declared explicitly
  • the logic stays near the entity
  • state transition rules are easier to scan
  • tracing behavior costs less than with scattered signals

What I want is not “something happens somewhere automatically.” I want code that makes it obvious when and why a model performs a side effect.

The Difference Grows in State-Heavy Systems

Orders, approvals, payouts, deliveries, memberships. These are all state machines in disguise.

In those systems, hidden rules become bugs quickly. That is why I prefer thinking in terms of entity lifecycle instead of loosely attached events.

So I try to minimize signals and keep transition rules close to the model. In that shape, django-lifecycle has been the better fit.