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.