CodingBowl

10 Python Patterns, Part 4: The Event-Driven Pattern in Django

Published on 14 Feb 2026 Tech Software Architecture
image
Photo by Alex Zaj on Unsplash

In our previous posts, we looked at how to build complex objects and swap behaviors. But as your Django project grows, you’ll notice a common problem: your logic starts getting bloated with "side effects." When a user signs up, you might need to:

  • Create a user profile.
  • Send a welcome email.
  • Sync data to an external CRM.
  • Trigger a Slack notification.

The Event-Driven Pattern solves this by letting your code broadcast that "something happened," allowing other parts of the system to react independently.


The Problem: The "God Function"

In a standard Django app, you might be tempted to put everything in the View or a single Service function. This creates tight coupling:

# The "Bad" Way - Tightly Coupled
def register_user(user_data):
    # Core Logic
    user = User.objects.create(**user_data)
    
    # Side Effects (Hard-coded)
    send_welcome_email(user.email)
    sync_to_salesforce(user)
    notify_slack_channel("#new-users", f"New user: {user.username}")
    
    return user

If the Salesforce API is down, your registration might fail. If you want to stop sending Slack notifications, you have to modify the core register_user function.


The Solution: The Event-Driven Approach

In Django, we use Signals to decouple these actions. The core function simply sends a signal that a user was created, and "listeners" handle the rest.

1. Defining the Signal

# signals.py
from django.dispatch import Signal

# We define a custom signal for clarity
user_registered_event = Signal()

2. The Refactored Service

# services.py
from .signals import user_registered_event

def register_user(user_data):
    user = User.objects.create(**user_data)
    
    # We just broadcast the event and move on
    user_registered_event.send(sender=user.__class__, user=user)
    
    return user

3. The Independent Listeners

Now, our side effects live in separate handlers. We can add or remove them without ever touching the register_user function again.

# handlers.py
from django.dispatch import receiver
from .signals import user_registered_event

@receiver(user_registered_event)
def handle_new_user_email(sender, user, **kwargs):
    send_welcome_email(user.email)

@receiver(user_registered_event)
def handle_salesforce_sync(sender, user, **kwargs):
    # This could also be a Celery task for async execution
    sync_to_salesforce.delay(user.id)

Why it's Better for Big Projects

  • Loose Coupling: The registration logic doesn't know (or care) that Salesforce exists.
  • Improved Performance: By using signals to trigger Celery tasks (as shown with .delay()), heavy API calls happen in the background.
  • Error Isolation: If the Slack notification fails, it won't crash the user registration process.

The Event-Driven pattern transitions your Django application from a linear, rigid script into a reactive system. By broadcasting events rather than calling functions directly, you create a codebase that is easier to extend and maintain.

Pro-tip for Scalability: To keep your architecture clean, use signals as "routers." Instead of writing heavy logic inside the handler, have the handler call a dedicated function in your Service Layer. This keeps side effects organized, prevents technical debt, and makes your background tasks significantly easier to unit test in isolation.

Meow! AI Assistance Note

This post was created with the assistance of Gemini AI and ChatGPT.
It is shared for informational purposes only and is not intended to mislead, cause harm, or misrepresent facts. While efforts have been made to ensure accuracy, readers are encouraged to verify information independently. Portions of the content may not be entirely original.

image
Photo by Yibo Wei on Unsplash