A Django senior engineer interview doesn’t test your knowledge of Django’s ORM or view patterns; it tests your ability to architect and scale complex web applications using Python’s most popular framework.
Let’s see Django in action, not in a tutorial, but in a real, albeit simplified, scenario. Imagine a high-traffic e-commerce site.
# models.py
from django.db import models
from django.contrib.auth.models import User
class Product(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
def __str__(self):
return self.name
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=50, default='Pending') # Pending, Shipped, Delivered, Cancelled
def __str__(self):
return f"Order {self.id} by {self.user.username}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
price_at_purchase = models.DecimalField(max_digits=10, decimal_places=2) # To capture price at the time of order
def __str__(self):
return f"{self.quantity} x {self.product.name} in Order {self.order.id}"
# views.py
from rest_framework import generics, status
from rest_framework.response import Response
from .models import Product, Order, OrderItem
from .serializers import ProductSerializer, OrderSerializer, OrderItemSerializer
from django.db import transaction
from django.shortcuts import get_object_or_404
class ProductListCreateView(generics.ListCreateAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
class ProductDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
class OrderCreateView(generics.CreateAPIView):
queryset = Order.objects.all()
serializer_class = OrderSerializer
@transaction.atomic
def perform_create(self, serializer):
order_data = serializer.validated_data
user = self.request.user # Assuming authentication is set up
order_items_data = order_data.pop('items', []) # Extract items from payload
if not order_items_data:
raise serializers.ValidationError("Order must contain at least one item.")
order_total = 0
order_instance = serializer.save(user=user)
for item_data in order_items_data:
product_id = item_data.get('product')
quantity = item_data.get('quantity')
if not product_id or not quantity or quantity <= 0:
raise serializers.ValidationError("Invalid product or quantity in order items.")
product = get_object_or_404(Product, pk=product_id)
if product.stock < quantity:
raise serializers.ValidationError(f"Not enough stock for {product.name}. Available: {product.stock}")
price_at_purchase = product.price
item_total = price_at_purchase * quantity
order_total += item_total
OrderItem.objects.create(
order=order_instance,
product=product,
quantity=quantity,
price_at_purchase=price_at_purchase
)
product.stock -= quantity
product.save()
order_instance.total_amount = order_total
order_instance.save()
serializer.instance = order_instance # Ensure instance is set for post-save signals etc.
# serializers.py (simplified)
from rest_framework import serializers
from .models import Product, Order, OrderItem
class OrderItemSerializer(serializers.ModelSerializer):
product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all())
class Meta:
model = OrderItem
fields = ['product', 'quantity'] # price_at_purchase is set by the view
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True) # Nested serializer for order items
class Meta:
model = Order
fields = ['id', 'user', 'created_at', 'total_amount', 'status', 'items']
read_only_fields = ['user', 'created_at', 'total_amount', 'status'] # These are managed by the view
This snippet shows a basic e-commerce setup: Product and Order models. The OrderCreateView is where the magic (and complexity) happens. It uses django.db.transaction.atomic to ensure that either the entire order is placed successfully, or none of it is. This is crucial: if an order is placed but stock isn’t deducted, or stock is deducted but the order isn’t recorded, the system is in an inconsistent state. The view also handles stock checks and updates, and calculates the total_amount dynamically.
The core problem this system solves is managing concurrent, stateful transactions in a web application. A senior engineer needs to think beyond individual requests. They’re concerned with:
- Scalability: How does this perform when 1000 users try to order the last item simultaneously?
- Reliability: What happens if the database connection drops mid-transaction?
- Maintainability: How easy is it to add new features, like discounts or shipping options, without breaking existing functionality?
- Security: How do we prevent users from manipulating prices or ordering items they shouldn’t?
Django provides tools, but the architecture is the engineer’s responsibility. This involves understanding database indexing, caching strategies (e.g., using Redis for session data or frequently accessed product lists), asynchronous task queues (like Celery for sending confirmation emails or processing complex reports), and potentially microservices if the application grows very large.
The transaction.atomic block is a fundamental tool, but it’s also a point of contention. If multiple requests try to decrement the stock of the same product, only one will succeed at a time. For extremely high-volume operations, a more sophisticated locking mechanism or a different approach might be needed. For instance, instead of directly decrementing product.stock, you might use a separate table to track pending stock deductions and process them asynchronously, or even rely on database-level row locking if the ORM doesn’t provide sufficient granular control.
When building complex systems, it’s easy to overlook the nuances of how Django’s ORM interacts with the database under the hood. For example, a seemingly simple product.save() inside a loop for OrderItem creation will issue a separate UPDATE query for each item. This is inefficient. A senior engineer would recognize this and batch the updates. You can achieve this by collecting all modified Product instances and then calling Product.objects.bulk_update(products_to_update, ['stock']) after the loop, significantly reducing database round trips.
The next challenge you’ll face is handling webhook integrations from payment gateways or shipping providers, which often require robust error handling and idempotency.