CodingBowl

10 Python Patterns, Part 9: The Specification Pattern in Django

Published on 18 Feb 2026 Tech Software Architecture
image
Photo by Simone Dinoia on Unsplash

In our previous post, we used the Command Pattern to encapsulate actions. But before you can execute a command, you often need to identify exactly which objects meet complex criteria. Usually, this ends up as messy .filter() calls scattered throughout the app. The Specification Pattern allows you to encapsulate a business rule into a single, reusable object that can be combined with others using boolean logic (AND, OR, NOT).

The Business Use Case: Highly Targeted Marketing

Imagine your marketing team wants to send a discount to a very specific group:

  1. Users who have spent over $500.
  2. AND who haven't logged in for 30 days.
  3. OR users who are "VIP" members.

If you hard-code this into a single View or Service, you can't reuse those individual "High Spender" or "VIP" rules elsewhere in the system.


The Problem: The "QuerySet Soup"

# The "Bad" Way: Hard-coded, non-reusable filtering
def get_marketing_targets():
    # This logic is trapped inside this function and is hard to read
    return User.objects.filter(
        (Q(total_spent__gt=500) & Q(last_login__lt=thirty_days_ago)) | 
        Q(is_vip=True)
    )

If you need the "High Spender" logic for a different report, you have to copy-paste the query logic. If that threshold changes to $600, you have to find and update every instance manually, which is a maintenance nightmare.


The Solution: The Specification Pattern

We wrap each business rule into its own Specification class. These classes can then be "chained" together to build complex queries dynamically while keeping the rules centralized.

Step 1: Define the Base Specification

We create a base class that allows us to use & and | operators to combine our business rules.

# specs/base.py
class Specification:
    def is_satisfied_by(self, obj):
        raise NotImplementedError()

    def __and__(self, other):
        return AndSpecification(self, other)

    def __or__(self, other):
        return OrSpecification(self, other)

class AndSpecification(Specification):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def is_satisfied_by(self, obj):
        return self.left.is_satisfied_by(obj) and self.right.is_satisfied_by(obj)

Step 2: Create Concrete Business Rules

Each business rule becomes a small, testable class that can be reused anywhere in the app.

# specs/user_specs.py
class HighSpenderSpec(Specification):
    def is_satisfied_by(self, user):
        return user.total_spent > 500

class InactiveSpec(Specification):
    def is_satisfied_by(self, user):
        return user.last_login < thirty_days_ago

class VIPSpec(Specification):
    def is_satisfied_by(self, user):
        return user.is_vip

Step 3: The New Workflow (Combining Rules)

Now, we can combine these rules like Lego blocks. The resulting code reads like a business requirement, not a database query.

# services.py
def get_marketing_targets():
    # Build the complex rule by combining smaller ones
    marketing_rule = (HighSpenderSpec() & InactiveSpec()) | VIPSpec()
    
    # Apply the rule to your data
    all_users = UserRepository.get_all()
    return [u for u in all_users if marketing_rule.is_satisfied_by(u)]

Why it's Better for Big Projects

  • Extreme Reusability: You define "HighSpender" once and use it across the entire application—from dashboard views to background cleanup tasks.
  • Declarative Logic: Your services describe what they are looking for (the business rule), not how to filter the database tables.
  • Simplified Testing: You can unit test each specification in isolation with simple mock objects, ensuring your business logic is 100% accurate.

Post Summary

The Specification Pattern turns rigid database queries into a flexible language of business rules. It is essential for any Django project that deals with complex user segmentation, risk assessment, or dynamic search filters.

Architecture Note: To keep your code clean and avoid circular import errors, store your business rules in a dedicated specifications.py file. This allows your Service Layer to use these rules directly without being coupled to the database structure, making your application significantly more robust and easier to maintain.

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