We have reached the official conclusion of our 10-part series! Over the last nine posts, we’ve built a robust, decoupled architecture. But as your Django project expands, you’ll face a final hurdle: how does the system find the right tool for the job without hard-coding every possibility? The Registry Pattern acts as a central "phone book" where different modules register themselves, allowing your core logic to remain clean and extensible.
The Business Use Case: A Multi-Provider Payment System
Imagine your platform supports Stripe, PayPal, and Adyen. Your users choose their preferred method at checkout. Instead of your checkout_service having a massive list of imports and if/else checks for every provider, it simply asks the PaymentRegistry for the handler it needs based on a simple ID string.
The Problem: The Endless Import & If-Else Chain
# The "Bad" Way: Hard-coded discovery
def get_payment_handler(provider_name):
# This list grows forever and requires manual updates in the core code
if provider_name == "stripe":
return StripeHandler()
elif provider_name == "paypal":
return PayPalHandler()
raise ValueError("Provider not found")
This approach is brittle. Every time you add a new payment provider, you have to modify the core get_payment_handler function, violating the Open-Closed Principle (code should be open for extension but closed for modification).
The Solution: The Registry Pattern
We create a central registry that stores a mapping of names to classes. Modules can "plug themselves in" using a simple decorator, and the core app never needs to know they exist until they are called at runtime.
Step 1: Create the Registry
The Registry is a simple container that stores our available handlers in a dictionary.
# registry.py
class PaymentRegistry:
_handlers = {}
@classmethod
def register(cls, name):
"""Decorator to register a handler class."""
def wrapper(handler_class):
cls._handlers[name] = handler_class
return handler_class
return wrapper
@classmethod
def get(cls, name):
"""Retrieve a new instance of the requested handler."""
handler = cls._handlers.get(name)
if not handler:
raise ValueError(f"Provider '{name}' not supported.")
return handler()
Step 2: Registering Providers (The "Plug-in" Step)
Each provider handles its own registration. Notice that we don't need to touch the registry code to add a new one; we just add the decorator to the new class.
# providers/stripe.py
from myapp.registry import PaymentRegistry
@PaymentRegistry.register("stripe")
class StripeHandler:
def process(self, amount):
print(f"Processing ${amount} via Stripe API")
# providers/paypal.py
@PaymentRegistry.register("paypal")
class PayPalHandler:
def process(self, amount):
print(f"Processing ${amount} via PayPal SDK")
Step 3: The New Workflow (Dynamic Discovery)
The Service Layer now stays small, fast, and completely agnostic of specific providers. It just asks for the "key."
# services.py
from .registry import PaymentRegistry
def checkout_service(provider_name, amount):
# The service just asks the registry for 'the tool'
handler = PaymentRegistry.get(provider_name)
handler.process(amount)
Why it's Better for Big Projects
- Plugin-Based Architecture: You can add features simply by dropping a new file into your project. If it has the
@registerdecorator, the system recognizes it automatically. - Decoupled Imports: Your main logic doesn't need to import every single provider, preventing circular dependencies and keeping memory usage low.
- Cleaner Tests: You can register "Mock" handlers in your test suite to simulate different behaviors without touching production code.
The Registry Pattern turns your Django application into a flexible, extensible platform. It is the final piece of the puzzle for any project aiming for true modularity and a "plug-and-play" architecture.
Architecture Note: To avoid circular imports, ensure your Registry is defined in a low-level module (like base/registry.py). This allows your Service Layer to request tools from the registry while keeping the individual implementations completely isolated from one another.