CodingBowl

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

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.

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