CodingBowl

10 Python Patterns, Part 5: The Repository Pattern in Django

Published on 15 Feb 2026 Tech Software Architecture
image
Photo by Jakub Żerdzicki on Unsplash

In our previous post, we explored how the Event-Driven pattern helps decouple side effects. Today, we address one of the most common sources of technical debt in Django: ORM leakage. In large projects, raw QuerySets scattered across views and services make the codebase rigid and difficult to test. The Repository Pattern acts as a mediator between your business logic and the database.


The Problem: Leaky Abstractions

Most Django developers perform database queries directly in their views or service layers. While convenient, it leads to duplicated logic and makes unit testing difficult because your logic is "married" to the database.

# The "Standard" Way - Logic mixed with QuerySet
def get_active_premium_users():
    # If this logic changes, you have to find every place 
    # where this specific filter is written.
    return User.objects.filter(
        is_active=True, 
        profile__is_premium=True,
        last_login__year=2024
    )

If you ever decide to switch a specific table to a different database (like MongoDB or an external API), you would have to rewrite every view that calls the ORM.


The Solution: The Repository Pattern

The Repository Pattern centralizes all data access. Your business logic asks the Repository for an object, and the Repository handles the "how" of fetching it.

1. Defining the Repository

# repositories.py
from django.contrib.auth.models import User

class UserRepository:
    @staticmethod
    def get_active_premium_users():
        return User.objects.filter(
            is_active=True, 
            profile__is_premium=True
        )

    @staticmethod
    def get_by_id(user_id):
        return User.objects.get(id=user_id)
        
    @staticmethod
    def update_last_login(user):
        user.save(update_fields=['last_login'])

2. Using the Repository in a Service

Now, your service layer doesn't need to know anything about Django's objects.filter(). It simply communicates with the Repository.

# services.py
from .repositories import UserRepository

def process_premium_announcements():
    # Business logic is now clean and readable
    users = UserRepository.get_active_premium_users()
    
    for user in users:
        send_premium_update(user)

Why it's Better for Big Projects

  • Centralized Logic: If the definition of an "Active User" changes, you only update the filter in the Repository, not in ten different files.
  • Easier Testing: You can easily "Mock" the Repository in your unit tests. Instead of hitting a real database, your test can simply return a list of dummy Python objects.
  • Storage Agnostic: If you move user profiles to an external microservice or a NoSQL database, your process_premium_announcements service won't even notice the change.

The Repository Pattern provides a clean separation between how your data is stored and how it is used. By abstracting the Django ORM, you create a system that is significantly easier to test and maintain as it grows.

Architecture Note: To keep your codebase clean and avoid circular import errors, ensure your Repositories stay focused purely on data access. Avoid putting heavy business logic inside the Repository itself. Instead, use your Service Layer to orchestrate the Repositories. This ensures your data access layer stays thin and your business rules stay centralized and testable.

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