DevHuzaifa's Portfolio
3 min readUpdated Aug 17, 2026

The Factory Pattern in Django: Creating Objects Dynamically

Learn how to implement the Factory Pattern in Django to centralize object creation, decouple business logic, and avoid confusion with the factory_boy testing library

#Django#System Design#Code Patterns

The Factory Pattern in Django: Creating Objects Dynamically

As Django applications scale, business logic frequently demands creating different objects depending on runtime context: user type, selected payment method, incoming webhook source, or preferred notification channel.

When object instantiation is hardcoded across views, serializers, and background tasks using scattered conditional blocks (if/elif/else), the codebase becomes tightly coupled and fragile. Modifying a single constructor parameter or adding a new provider forces changes across multiple files.

The Factory Pattern solves this problem by encapsulating and centralizing object creation behind a unified interface.

Factory Pattern vs. factory_boy: Clearing the Confusion

Django developers encounter the word "factory" in two distinct contexts. While they share a naming convention around creation, they serve completely different purposes in the development lifecycle.

Category

Factory Pattern: Gang of Four (GoF) Creational Pattern

factory_boy: Testing utility for fixtures and test data

Where It Lives

Factory Pattern: Production code, typically in services.py or factories.py

factory_boy: Test suite, typically in tests/factories.py

Primary Purpose

Factory Pattern: Dynamically selects and instantiates concrete classes based on application state.

factory_boy: Generates model instances and mock data for unit and integration tests.

Target Objects

Factory Pattern: Service handlers, API clients, storage adapters, notification senders.

factory_boy: Django ORM models such as User, Order, and Profile.

Example

Factory Pattern: Routes an order to StripeGateway or PayPalGateway.

factory_boy: Creates 10 dummy User records with fake emails for pytest.

python
# factory_boy: USED ONLY IN TESTS
# tests/factories.py
import factory
from myapp.models import User

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
    username = factory.Faker('user_name')
    email = factory.Faker('email')

# Factory Pattern: USED IN PRODUCTION BUSINESS LOGIC
# services/factories.py
class PaymentGatewayFactory:
    @staticmethod
    def get_gateway(provider_name: str) -> BasePaymentGateway:
        ...

How Factories Centralize Object Creation

The Factory Pattern delegates the responsibility of object instantiation to a specialized factory class or method. The calling code (such as a Django view or Celery task) only depends on an abstract interface, not concrete implementations.

factory

Notification Factory Implementation Example


Practical Implementation: Dynamic Notification System

Consider a Django notification service that delivers messages across multiple channels: Email, SMS, and Mobile Push.

1. Define the Common Interface

Create an abstract base class ensuring every notification channel adheres to the same contract:

python
# notifications/interfaces.py
from abc import ABC, abstractmethod

class BaseNotificationSender(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str, **kwargs) -> bool:
        """Deliver the notification. Must be implemented by all concrete senders."""
        pass

2. Implement Concrete Senders

Each channel encapsulates its own dependencies, API keys, and payload formatting:

python
# notifications/senders.py
import logging
from .interfaces import BaseNotificationSender

logger = logging.getLogger(__name__)

class EmailNotificationSender(BaseNotificationSender):
    def send(self, recipient: str, message: str, **kwargs) -> bool:
        subject = kwargs.get("subject", "System Notification")
        logger.info(f"Sending Email to {recipient} with subject '{subject}': {message}")
        # Call django.core.mail or external service (SendGrid, SES)
        return True

class SMSNotificationSender(BaseNotificationSender):
    def send(self, recipient: str, message: str, **kwargs) -> bool:
        logger.info(f"Sending SMS via Twilio to {recipient}: {message}")
        # Call Twilio / MessageBird API
        return True

class PushNotificationSender(BaseNotificationSender):
    def send(self, recipient: str, message: str, **kwargs) -> bool:
        device_token = kwargs.get("device_token")
        logger.info(f"Sending Push Notification to device {device_token}: {message}")
        # Call Firebase Cloud Messaging (FCM) / Apple APNs
        return True

3. Build the Factory

Using a registry dictionary provides clean dispatching without rigid if/elif blocks and allows new senders to be registered dynamically:

python
# notifications/factory.py
from typing import Dict, Type
from .interfaces import BaseNotificationSender
from .senders import (
    EmailNotificationSender,
    SMSNotificationSender,
    PushNotificationSender
)

class NotificationFactory:
    _registry: Dict[str, Type[BaseNotificationSender]] = {
        "email": EmailNotificationSender,
        "sms": SMSNotificationSender,
        "push": PushNotificationSender,
    }

    @classmethod
    def register_sender(cls, channel: str, sender_cls: Type[BaseNotificationSender]) -> None:
        """Allow plugins or apps to register custom notification channels."""
        cls._registry[channel.lower()] = sender_cls

    @classmethod
    def create(cls, channel: str) -> BaseNotificationSender:
        """Instantiate and return the requested notification sender."""
        sender_class = cls._registry.get(channel.lower())
        if not sender_class:
            valid_channels = ", ".join(cls._registry.keys())
            raise ValueError(f"Unknown notification channel '{channel}'. Supported: {valid_channels}")
        return sender_class()

4. Consume in Django Views or Tasks

The caller remains completely agnostic of individual delivery mechanics:

python
# views.py or tasks.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .factory import NotificationFactory

class SendAlertView(APIView):
    def post(self, request):
        channel = request.data.get("channel")  # 'email', 'sms', or 'push'
        recipient = request.data.get("recipient")
        message = request.data.get("message")
        extra_kwargs = request.data.get("metadata", {})

        try:
            sender = NotificationFactory.create(channel)
            success = sender.send(recipient=recipient, message=message, **extra_kwargs)
            return Response({"success": success}, status=status.HTTP_200_OK)
        except ValueError as exc:
            return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)

Other Real-World Django Use Cases

  • Payment Gateways: Switching between StripeProvider, PayPalProvider, or AdyenProvider based on tenant configuration, country code, or currency.
  • Storage Providers: Instantiating S3StorageService, GCSStorageService, or LocalStorageService dynamically depending on environment settings or data classification.
  • Report Generators: Exporting data models into PDFReportGenerator, CSVReportGenerator, or ExcelReportGenerator using a unified .generate() signature.
  • User & Profile Provisioning: Generating differentiated onboarding workflows for BuyerProfile, SellerProfile, or EnterpriseAdmin based on role definitions.

Architectural Advantages

  • Open-Closed Principle (OCP): Add new notification channels (e.g., WhatsApp or Slack) by creating a new subclass and registering it in the factory without altering existing views.
  • Single Responsibility Principle (SRP): Object creation is decoupled from runtime domain logic.
  • Testability: Unit tests for views only require mocking the factory method rather than mocking every concrete API client constructor.