CodingBowl

10 Python Patterns, Part 6: The Mapper Pattern in Django

Published on 15 Feb 2026 Tech Software Architecture
image
Photo by clinton martel on Unsplash

10 Python Patterns, Part 6: The Mapper Pattern in Django

In our previous post, we used the Repository Pattern to hide where data comes from. However, even with Repositories, we often still pass "Fat" Django ORM models into our business logic. This creates a "leaky abstraction" where your business logic depends on the database structure. The Mapper Pattern solves this by converting database rows into clean, simple Python objects.


The Business Use Case: A Loyalty Discount System

Imagine you are building a loyalty system. A user gets a discount based on their "Premium" status and their total lifetime spend. In a standard Django app, this logic lives inside a service that takes a User model instance. Because the model is "live," any logic you write is tethered to the database schema.


The Problem: The "Fat" Model Dependency

Django models are powerful, but they are "heavy." They carry database connections and hidden state. If your business logic expects a Django User object, you cannot easily test it without a database.

The WorkFlow: Before the Mapper Pattern

In this workflow, the service layer is married to the ORM. Testing this requires a database hit because the model is a live object.

# services.py
def calculate_loyalty_discount(user_model):
    # The "Bad" Way: Logic is tied to the ORM model
    # This triggers a database sub-query if 'profile' isn't prefetched!
    if user_model.profile.is_premium and user_model.total_spent > 1000:
        return 0.20
    return 0.05

The Solution: The Mapper Pattern

The Mapper Pattern takes data from the Repository and "maps" it into a Domain Object. Your business logic only knows about this clean object.

Step 1: Define the Domain Object

We create a "Domain Entity"—a pure Python class with no knowledge of Django.

# domain.py
from dataclasses import dataclass

@dataclass
class UserEntity:
    id: int
    is_premium: bool
    total_spent: float
    email: str

Step 2: Create the Mapper Logic

The Mapper translates the "dirty" Django Model into our "clean" Domain Entity.

# mappers.py
from .domain import UserEntity

class UserMapper:
    @staticmethod
    def to_domain(user_model) -> UserEntity:
        return UserEntity(
            id=user_model.id,
            is_premium=user_model.profile.is_premium,
            total_spent=user_model.total_spent,
            email=user_model.email
        )

Step 3: The New Workflow (The "Clean" Way)

The Repository uses the Mapper, and the Service consumes the clean Entity. Notice how the service no longer cares about the database.

# repositories.py
class UserRepository:
    def get_user_entity(user_id) -> UserEntity:
        user_model = User.objects.get(id=user_id)
        return UserMapper.to_domain(user_model)

# services.py
def calculate_loyalty_discount(user_entity: UserEntity):
    # Pure Python logic. No DB hits. No prefetches required.
    if user_entity.is_premium and user_entity.total_spent > 1000:
        return 0.20
    return 0.05

Why it's Better for Big Projects

  • Pure Business Logic: Your services work with simple Python classes. They don't care about .save() or .filter().
  • Lightning Fast Tests: You can test your logic by passing a simple UserEntity(id=1, is_premium=True, total_spent=1500, email="test@test.com"). No database or migrations required.
  • Strict Contracts: Using tools like Pydantic for your domain objects ensures data is validated the moment it enters your logic layer.

The Mapper Pattern ensures that your core business rules are protected from the ever-changing structure of your database. By translating "Database Models" into "Domain Entities," you achieve a level of decoupling essential for enterprise-grade applications.

Architecture Note: To keep your codebase clean, avoid putting business logic inside your Mappers. Mappers should be "dumb" translators. For scalability, ensure your Service Layer receives these clean Entities to perform calculations. This prevents "leaky abstractions" and makes your background tasks and unit tests significantly easier to manage.

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