10 Python Patterns, Part 10: The Registry Pattern in Django

Published on 18 Feb 2026 Tech Software Architecture
image
Photo by Scott Greer on Unsplash
Disclaimer: This post was created with the assistance of Gemini and/or ChatGPT for informational purposes. While given a quick look over, readers are encouraged to verify key facts independently.

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 @register decorator, 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.

Comments