CodingBowl

10 Python Patterns, Part 1: Dependency Injection (The Right Way in Django)

Published on 21 Jan 2026 Tech Software Architecture
image
Photo by Almas Salakhov on Unsplash

Learning these 10 Python patterns helps you avoid architectural mistakes as projects grow. As Hassan Nauman explains in his article “10 Python Patterns Every Developer Should Learn Before Building Big Projects”, syntax isn’t the real problem — structure is. These patterns reduce coupling, manage complexity, and help keep code maintainable and scalable over time.

  • Dependency Injection – Pass dependencies in instead of hard-coding them, making code easier to test and change.
  • Strategy Pattern – Replace large if/else blocks with interchangeable behaviors.
  • Builder Pattern – Construct complex objects step by step without messy constructors.
  • Event-Driven Pattern – Decouple components by reacting to events instead of direct calls.
  • Repository Pattern – Isolate database logic from business logic.
  • Mapper Pattern – Convert raw data (dicts/JSON) into clean domain objects.
  • Pipeline Pattern – Process data through clear, ordered stages.
  • Command Pattern – Encapsulate actions so they can be executed and undone.
  • Specification Pattern – Keep complex filtering and rules readable and reusable.
  • Registry Pattern – Build flexible plugin systems without hard-coded conditionals.

What Is Dependency Injection?

Dependency Injection is the foundation of maintainable architecture. In Django projects, it prevents fat views, hard-coded services, and untestable logic.

Dependency Injection means passing dependencies into a class instead of creating them inside the class. This reduces coupling and makes your code easier to test and change.

Step 1: Create the Model


from django.db import models

class Customer(models.Model):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

Step 2: Create the Repository

The repository owns all database access and hides Django ORM details.


from .models import Customer

class CustomerRepository:
    def get_by_id(self, pk):
        return Customer.objects.get(pk=pk)

    def create(self, email):
        return Customer.objects.create(email=email)

    def update(self, pk, email, is_active):
        customer = Customer.objects.get(pk=pk)
        customer.email = email
        customer.is_active = is_active
        customer.save()
        return customer

Step 3: Create the Service

The service contains business rules and receives the repository via Dependency Injection.


class CustomerService:
    def __init__(self, repository):
        self.repository = repository

    def register_customer(self, email):
        return self.repository.create(email)

    def update_customer(self, pk, email, is_active):
        if not email.endswith("@example.com"):
            raise ValueError("Only example.com emails are allowed")
        return self.repository.update(pk, email, is_active)

Step 4: Create a Form for Validation

Forms validate input only. They do not contain business rules or persistence logic.


from django import forms

class CustomerUpdateForm(forms.Form):
    email = forms.EmailField()
    is_active = forms.BooleanField(required=False)

Step 5: Create Class-Based Views

At this point, clear responsibilities are established: the repository handles persistence, the service enforces business rules, and the form validates input. The views simply coordinate these pieces.


from django.views import View
from django.shortcuts import render, redirect
from django.http import Http404, JsonResponse
from .forms import CustomerUpdateForm
from .repositories import CustomerRepository
from .services import CustomerService
from .models import Customer

class RegisterCustomerView(View):
    repository_class = CustomerRepository
    service_class = CustomerService

    def post(self, request):
        repo = self.repository_class()
        service = self.service_class(repo)
        customer = service.register_customer(request.POST.get("email"))
        return JsonResponse({"id": customer.id})

class UpdateCustomerView(View):
    repository_class = CustomerRepository
    service_class = CustomerService
    form_class = CustomerUpdateForm
    template_name = "customers/update.html"

    def get(self, request, pk):
        customer = Customer.objects.get(pk=pk)
        form = self.form_class(initial={
            "email": customer.email,
            "is_active": customer.is_active,
        })
        return render(request, self.template_name, {"form": form})

    def post(self, request, pk):
        form = self.form_class(request.POST)

        if not form.is_valid():
            return render(request, self.template_name, {"form": form})

        repo = self.repository_class()
        service = self.service_class(repo)

        try:
            service.update_customer(
                pk,
                form.cleaned_data["email"],
                form.cleaned_data.get("is_active", False),
            )
        except ValueError as e:
            form.add_error(None, str(e))
            return render(request, self.template_name, {"form": form})

        return redirect("customer-detail", pk=pk)

Step 6: Add Views to urls.py


from django.urls import path
from .views import RegisterCustomerView, UpdateCustomerView

urlpatterns = [
    path("customers/register/", RegisterCustomerView.as_view()),
    path("customers/update//", UpdateCustomerView.as_view()),
]

Step 7: Testing with Dependency Injection

Because dependencies are injected, we can replace the real repository with a fake implementation that follows the same interface. This allows us to test behavior without touching the database.


class FakeCustomerRepository:
    def __init__(self):
        self.updated = False

    def create(self, email):
        return {"email": email}

    def update(self, pk, email, is_active):
        self.updated = True
        return True

from django.test import TestCase
from django.urls import reverse
from app.views import UpdateCustomerView

class UpdateCustomerViewTest(TestCase):

    def setUp(self):
        self.fake_repo = FakeCustomerRepository()
        UpdateCustomerView.repository_class = lambda: self.fake_repo

    def test_update_customer_calls_repository(self):
        response = self.client.post(
            reverse("update-customer", args=[1]),
            {"email": "test@example.com", "is_active": True}
        )
        self.assertEqual(response.status_code, 302)
        self.assertTrue(self.fake_repo.updated)

What Dependency Injection Gave Us

  • Clear separation of concerns
  • Proper form validation without UpdateView
  • Business rules isolated in services
  • Simple, fast tests

Dependency Injection is not about adding layers everywhere. It is about keeping control over how your code grows.

In the next post, we will explore the Strategy Pattern and how it replaces complex conditional logic in Django projects.

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