The Goal: Code That Survives Growth
Most Django projects start clean and become unmaintainable within 6 months. I have refactored enough legacy Django codebases to know what patterns survive and which ones don't. Here is the structure I use for every production project.
The Directory Structure
project/
+-- config/ # Project configuration
| +-- settings/
| | +-- base.py # Shared settings
| | +-- development.py # Dev overrides
| | +-- production.py # Production settings
| | +-- test.py # Test settings
| +-- urls.py
| +-- wsgi.py
+-- apps/
| +-- users/ # User management
| | +-- models.py
| | +-- services.py # Business logic
| | +-- selectors.py # Query logic
| | +-- views.py # HTTP layer only
| | +-- serializers.py
| | +-- tests/
| +-- products/
| +-- orders/
+-- common/ # Shared utilities
| +-- models.py # Base models
| +-- pagination.py
+-- requirements/
| +-- base.txt
| +-- dev.txt
| +-- prod.txt
+-- manage.py
Settings Split
Never use a single settings.py for all environments. Split it:
# config/settings/base.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
INSTALLED_APPS = [
'django.contrib.admin',
# ...
'apps.users',
'apps.products',
]
# config/settings/production.py
from .base import *
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'HOST': os.environ['DB_HOST'],
}
}
SECURE_HSTS_SECONDS = 31536000
The Service Layer Pattern
This is the most important pattern. Keep your views thin — they handle HTTP. Business logic lives in services:
# apps/orders/services.py
from django.db import transaction
def create_order(*, customer, items: list[dict]) -> Order:
with transaction.atomic():
order = Order.objects.create(customer=customer, status='pending')
total = 0
for item_data in items:
product = get_product_by_id(item_data['product_id'])
if product.stock < item_data['quantity']:
raise ValueError(f"{product.name} out of stock")
OrderItem.objects.create(
order=order,
product=product,
quantity=item_data['quantity'],
price=product.price
)
total += product.price * item_data['quantity']
product.stock -= item_data['quantity']
product.save()
order.total = total
order.save()
return order
Selectors for Query Logic
Separate read operations into selectors. This makes queries reusable and testable:
# apps/orders/selectors.py
from django.db.models import QuerySet, Sum
def get_orders_for_customer(*, customer_id: int, status: str = None) -> QuerySet:
qs = Order.objects.filter(customer_id=customer_id)
if status:
qs = qs.filter(status=status)
return qs.select_related('customer').prefetch_related('items')
def get_monthly_revenue(*, year: int, month: int) -> dict:
return Order.objects.filter(
created_at__year=year,
created_at__month=month,
status='completed'
).aggregate(total=Sum('total'))
Views: HTTP Layer Only
class OrderCreateView(APIView):
def post(self, request):
serializer = OrderCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
order = create_order(
customer=request.user,
items=serializer.validated_data['items']
)
return Response(OrderSerializer(order).data, status=201)
Testing Strategy
With this structure, testing is straightforward:
class CreateOrderTest(TestCase):
def setUp(self):
self.customer = create_test_user()
self.product = create_test_product(stock=10, price=25.00)
def test_creates_order_with_items(self):
order = create_order(
customer=self.customer,
items=[{'product_id': self.product.id, 'quantity': 2}]
)
assert order.total == 50.00
assert order.items.count() == 1
def test_rejects_out_of_stock(self):
with self.assertRaises(ValueError):
create_order(
customer=self.customer,
items=[{'product_id': self.product.id, 'quantity': 999}]
)
Key Principles
- Fat services, thin views — views handle HTTP, services handle logic
- Selectors for reads — all query logic in one place, easy to optimize
- Explicit over implicit — keyword-only arguments prevent positional bugs
- One app per domain — don't shove everything into one app
This structure has served me well across SaaS platforms, e-commerce systems, and client projects. It scales from solo developer to team without major refactoring.