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
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.
# 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.

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:
# 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."""
pass2. Implement Concrete Senders
Each channel encapsulates its own dependencies, API keys, and payload formatting:
# 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 True3. Build the Factory
Using a registry dictionary provides clean dispatching without rigid if/elif blocks and allows new senders to be registered dynamically:
# 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:
# 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, orAdyenProviderbased on tenant configuration, country code, or currency. - Storage Providers: Instantiating
S3StorageService,GCSStorageService, orLocalStorageServicedynamically depending on environment settings or data classification. - Report Generators: Exporting data models into
PDFReportGenerator,CSVReportGenerator, orExcelReportGeneratorusing a unified.generate()signature. - User & Profile Provisioning: Generating differentiated onboarding workflows for
BuyerProfile,SellerProfile, orEnterpriseAdminbased 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.