Skip to content
Software Architecture

10 Python Patterns, Part 3: Builder Pattern for Big Projects

The Builder Pattern helps manage complex object creation without overloading models, views, or forms. In Django projects, it is especially useful when assembling objects from multiple inputs, conditional steps, or external data sources. This post shows how the Builder Pattern keeps creation logic ex

CodingBowl··2 min read
Photo by Alex Zaj on Unsplash

As Django applications grow, object creation often becomes complex. Models, services, and view logic start sharing responsibility for assembling data. The Builder Pattern solves this by separating how an object is constructed from how it is used, keeping code readable, testable, and flexible.

What Problem the Builder Pattern Solves

When creating complex objects with optional fields, defaults, and validation rules, constructors and views quickly become cluttered. The Builder Pattern centralizes construction logic into a dedicated builder.

The Anti-Pattern

`
class CreateOrderView(View):
def post(self, request):
order = Order(
customer_id=request.POST.get("customer_id"),
shipping_address=request.POST.get("shipping_address"),
discount_code=request.POST.get("discount_code"),
is_expedited=bool(request.POST.get("expedited")),
created_by=request.user,
)
order.calculate_totals()
order.save()
`

The view is responsible for assembling, validating, and finalizing the object.

Step 1: Define the Builder Interface

`
class OrderBuilder:
def set_customer(self, customer):
raise NotImplementedError

def set_shipping(self, address):
raise NotImplementedError

def apply_discount(self, code):
raise NotImplementedError

def set_expedited(self, value):
raise NotImplementedError

def build(self):
raise NotImplementedError
`

Step 2: Implement a Concrete Builder

`
from .models import Order

class DjangoOrderBuilder(OrderBuilder):
def __init__(self):
self.order = Order()

def set_customer(self, customer):
self.order.customer = customer
return self

def set_shipping(self, address):
self.order.shipping_address = address
return self

def apply_discount(self, code):
if code:
self.order.discount_code = code
return self

def set_expedited(self, value):
self.order.is_expedited = value
return self

def build(self):
self.order.calculate_totals()
self.order.save()
return self.order
`

The builder exposes a fluent API and owns all construction logic.

Step 3: Use the Builder in a Service

`
class OrderCreationService:
def __init__(self, builder):
self.builder = builder

def create(self, data, user):
return (
self.builder
.set_customer(data["customer"])
.set_shipping(data["shipping_address"])
.apply_discount(data.get("discount_code"))
.set_expedited(data.get("expedited", False))
.build()
)
`

The service orchestrates the steps without knowing implementation details.

Step 4: Use the Builder in a Class-Based View

`
from django.views import View
from django.http import JsonResponse
from .builders import DjangoOrderBuilder
from .services import OrderCreationService

class CreateOrderView(View):
builder_class = DjangoOrderBuilder
service_class = OrderCreationService

def post(self, request):
builder = self.builder_class()
service = self.service_class(builder)

order = service.create(
data=request.POST,
user=request.user,
)

return JsonResponse({
"order_id": order.id,
"status": order.status,
})
`

Step 5: Testing the Builder Independently

`
class FakeOrderBuilder:
def __init__(self):
self.called = []

def set_customer(self, customer):
self.called.append("customer")
return self

def set_shipping(self, address):
self.called.append("shipping")
return self

def apply_discount(self, code):
self.called.append("discount")
return self

def set_expedited(self, value):
self.called.append("expedited")
return self

def build(self):
return "order"

def test_order_creation_flow():
builder = FakeOrderBuilder()
service = OrderCreationService(builder)

result = service.create(
data={
"customer": 1,
"shipping_address": "SG",
"discount_code": None,
},
user=None,
)

assert result == "order"
assert "customer" in builder.called
`

Why Builder Works Well in Django

  • Keeps object creation out of views

  • Encapsulates defaults and invariants

  • Supports complex, optional construction flows

  • Improves testability and reuse

Architectural Insight

If constructing an object requires more than a few parameters, or involves conditional logic and side effects, it is no longer a constructor problem. The Builder Pattern makes object creation explicit and intentional.