Taming Django Side Effects
First off, a definition: if you came here hoping for a lesson in functional programming, I apologise for my loose use of terminology - I'm not talking about those side effects.The side effects I am referring to are what I would term external side effects - which we are defining as those that effect systems outside of the Django ORM transaction boundary - or in plain english, remote APIs.
At YunoJuno we integrate with a lot of external services, e.g.: Mailchimp, Mandrill, HipChat, Base CRM, Highrise, Onfido, Twilio, and more. Our integration points with these services vary, but are generally related to state transitions - e.g. if someone registers we create various Django objects, but then we also alert our account team on HipChat, push some data to our sales CRM system (Base), and as well as sending confirmation emails. All of these integrations can be defined by a common set of attributes:
- They integrate with an external API
- They can be processed asynchronously (queued)
- They can be replayed (and are idempotent)
- They are not time critical (within reason)
- They can be executed in any order
- They do not affect the underlying state of the Django application
As the platform has grown, so have these side effects - we now have 130 transactional email templates - and they occur at different levels - in view functions, model methods, signal handlers. This makes them extremely hard to keep track of, and resulted in a codebase that was 50% managing non-core side effects:
def foo():
# do the thing the function is supposed to do
update_object(obj)
# spend the rest of the function working out which side-effects to fire
if settings.notify_account_handler:
send_notification(obj.account_handler)
if obj.has_changed_foo():
update_crm(obj)
In order to try and tame these side-effects, to make them easier to manage, document, test and control, we present django-side-effects
. You can read about the implementation over on Github, but in summary it provides a pattern for managing side-effects, as well as a management command that enables them to be self-documenting.
$ python manage.py display_side_effects
The following side-effects are registered:
update_profile:
- Update CRM system.
- Notify account managers.
close_account:
- Send confirmation email to user.
- Notify customer service.
We are now using this in production, and in the process of moving all our side-effects over to the new pattern. In addition to the self-documentation aspect, pulling out the side-effects into smaller single-responsibility function makes them easier to test (in isolation), easier to mock when testing other aspects, and easier to control (turn off/on).
Contributions welcome, as ever.
Making Freelance Work