Building a Service Marketplace with Django: Lessons from Netfix
As developers, we're constantly searching for the right tools to bring our ideas to life efficiently. When I decided to build Netfix, a service marketplace connecting companies with customers, I chose Django - and discovered features that make it uniquely powerful for complex applications. In this article, I'll share deep technical insights from building Netfix, highlighting Django features that aren't commonly covered in tutorials but can dramatically improve your development workflow Project Overview: Netfix Netfix is a platform where: Companies create profiles and list their services Customers browse and request services Each entity has personalized dashboards Analytics track and display trending services The system handles complex relationships between users, services, and requests Let's dive into some of the most powerful features I leveraged 1. Custom User Models - The Right Way Django's default user model works for simple applications, but for Netfix, I needed to support different user types with specific attributes. Here's how I implemented a custom user model: from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.db import models class UserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: raise ValueError('Email is required') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) return self.create_user(email, password, **extra_fields) class User(AbstractBaseUser, PermissionsMixin): USER_TYPE_CHOICES = ( ('customer', 'Customer'), ('company', 'Company'), ) email = models.EmailField(unique=True) name = models.CharField(max_length=255) user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) date_joined = models.DateTimeField(auto_now_add=True) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['name', 'user_type'] def __str__(self): return self.email Then I created separate profile models for each user type: # accounts/models.py class CompanyProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='company_profile') company_name = models.CharField(max_length=255) description = models.TextField() logo = models.ImageField(upload_to='company_logos/', null=True, blank=True) website = models.URLField(null=True, blank=True) def __str__(self): return self.company_name class CustomerProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile') phone_number = models.CharField(max_length=20, null=True, blank=True) address = models.TextField(null=True, blank=True) def __str__(self): return self.user.name Pro tip: When creating a custom user model, do it at the beginning of your project. Changing the user model mid-project is extremely challenging. 2. Signals for Automatic Profile Creation One feature I love about Django is signals. They allow you to trigger actions when certain events occur. I used signals to automatically create appropriate profiles when a user registers: from django.db.models.signals import post_save from django.dispatch import receiver from .models import User, CompanyProfile, CustomerProfile @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: if instance.user_type == 'company': CompanyProfile.objects.create(user=instance) elif instance.user_type == 'customer': CustomerProfile.objects.create(user=instance) @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): if instance.user_type == 'company': instance.company_profile.save() elif instance.user_type == 'customer': instance.customer_profile.save() 3. Advanced QuerySets with Annotations for Service Analytics For the trending services feature, I needed to count service requests and display them by popularity. Django's ORM provides powerful annotation capabilities: from django.db.models import Count, F, ExpressionWrapper, fields from django.db.models.functions import Now, ExtractDay from datetime import timedelta from .models import Service, ServiceRequest def trending_services(request): # Calculate trending services based on request count in last 30 days thirty_days_ago = timezone.now() - timedelta(days=30) trending = Se

As developers, we're constantly searching for the right tools to bring our ideas to life efficiently. When I decided to build Netfix, a service marketplace connecting companies with customers, I chose Django - and discovered features that make it uniquely powerful for complex applications.
In this article, I'll share deep technical insights from building Netfix, highlighting Django features that aren't commonly covered in tutorials but can dramatically improve your development workflow
Project Overview: Netfix
Netfix is a platform where:
- Companies create profiles and list their services
- Customers browse and request services
- Each entity has personalized dashboards
- Analytics track and display trending services
- The system handles complex relationships between users, services, and requests
Let's dive into some of the most powerful features I leveraged
1. Custom User Models - The Right Way
Django's default user model works for simple applications, but for Netfix, I needed to support different user types with specific attributes. Here's how I implemented a custom user model:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
class UserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('Email is required')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
USER_TYPE_CHOICES = (
('customer', 'Customer'),
('company', 'Company'),
)
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
user_type = models.CharField(max_length=10, choices=USER_TYPE_CHOICES)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'user_type']
def __str__(self):
return self.email
Then I created separate profile models for each user type:
# accounts/models.py
class CompanyProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='company_profile')
company_name = models.CharField(max_length=255)
description = models.TextField()
logo = models.ImageField(upload_to='company_logos/', null=True, blank=True)
website = models.URLField(null=True, blank=True)
def __str__(self):
return self.company_name
class CustomerProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
phone_number = models.CharField(max_length=20, null=True, blank=True)
address = models.TextField(null=True, blank=True)
def __str__(self):
return self.user.name
Pro tip: When creating a custom user model, do it at the beginning of your project. Changing the user model mid-project is extremely challenging.
2. Signals for Automatic Profile Creation
One feature I love about Django is signals. They allow you to trigger actions when certain events occur. I used signals to automatically create appropriate profiles when a user registers:
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, CompanyProfile, CustomerProfile
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
if instance.user_type == 'company':
CompanyProfile.objects.create(user=instance)
elif instance.user_type == 'customer':
CustomerProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
if instance.user_type == 'company':
instance.company_profile.save()
elif instance.user_type == 'customer':
instance.customer_profile.save()
3. Advanced QuerySets with Annotations for Service Analytics
For the trending services feature, I needed to count service requests and display them by popularity. Django's ORM provides powerful annotation capabilities:
from django.db.models import Count, F, ExpressionWrapper, fields
from django.db.models.functions import Now, ExtractDay
from datetime import timedelta
from .models import Service, ServiceRequest
def trending_services(request):
# Calculate trending services based on request count in last 30 days
thirty_days_ago = timezone.now() - timedelta(days=30)
trending = Service.objects.annotate(
request_count=Count('servicerequest',
filter=models.Q(servicerequest__created_at__gte=thirty_days_ago))
).order_by('-request_count')[:10]
return render(request, 'services/trending.html', {'trending_services': trending})
4. Custom Template Tags for Dynamic UI Elements
One lesser-known Django feature is custom template tags. I created a template tag to display service status with appropriate styling:
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter
def status_badge(status):
colors = {
'pending': 'warning',
'accepted': 'info',
'in_progress': 'primary',
'completed': 'success',
'declined': 'danger'
}
color = colors.get(status, 'secondary')
return mark_safe(f'"badge bg-{color}">{status.replace("_", " ").title()}')
Then in templates:
{% load service_extras %}
{{ service_request.status|status_badge }}
5. Class-Based Views with Mixins for Controlled Access
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import ListView, DetailView, CreateView, UpdateView
class CompanyRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_authenticated and self.request.user.user_type == 'company'
class ServiceCreateView(LoginRequiredMixin, CompanyRequiredMixin, CreateView):
model = Service
fields = ['name', 'description', 'price', 'category']
template_name = 'services/service_form.html'
def form_valid(self, form):
form.instance.company = self.request.user.company_profile
return super().form_valid(form)
6. Using Select Related and Prefetch Related for Performance
One challenge with relational data is the "N+1 query problem." Django's select_related and prefetch_related provide elegant solutions:
# Without optimization - this causes N+1 queries
def service_requests(request):
requests = ServiceRequest.objects.filter(service__company__user=request.user)
# Each time we access requests.service, we make a new query
return render(request, 'services/requests.html', {'requests': requests})
# With optimization
def service_requests_optimized(request):
requests = ServiceRequest.objects.filter(
service__company__user=request.user
).select_related(
'service', 'customer'
).prefetch_related(
'service__category'
)
# Now all related data is fetched in just 3 queries
return render(request, 'services/requests.html', {'requests': requests})
7. REST Framework ViewSets for API Development
To provide a robust API for third-party integrations and future platform extensibility, I built a comprehensive API using Django REST Framework's ViewSets:
from rest_framework import viewsets, permissions
from .serializers import ServiceSerializer, ServiceRequestSerializer
from services.models import Service, ServiceRequest
class IsCompanyOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_authenticated and request.user.user_type == 'company'
class ServiceViewSet(viewsets.ModelViewSet):
serializer_class = ServiceSerializer
permission_classes = [IsCompanyOrReadOnly]
def get_queryset(self):
queryset = Service.objects.all()
category = self.request.query_params.get('category', None)
if category:
queryset = queryset.filter(category__slug=category)
return queryset
def perform_create(self, serializer):
serializer.save(company=self.request.user.company_profile)
8. Advanced Form Processing with FormSets
For services with multiple attributes, I used Django's formsets:
from django.forms import inlineformset_factory
from .models import Service, ServiceAttribute
def service_create_with_attributes(request):
AttributeFormSet = inlineformset_factory(
Service, ServiceAttribute,
fields=('name', 'value'),
extra=3, can_delete=True
)
if request.method == 'POST':
form = ServiceForm(request.POST)
if form.is_valid():
service = form.save(commit=False)
service.company = request.user.company_profile
service.save()
formset = AttributeFormSet(request.POST, instance=service)
if formset.is_valid():
formset.save()
return redirect('service-detail', pk=service.pk)
else:
form = ServiceForm()
formset = AttributeFormSet()
return render(request, 'services/service_with_attributes.html', {
'form': form,
'formset': formset
})
9. Leveraging Django Admin for Quick Internal Tools
Django's admin site is incredibly powerful. I customized it for internal management:
from django.contrib import admin
from .models import Service, ServiceRequest, Category
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
list_display = ('name', 'company', 'price', 'category', 'is_active')
list_filter = ('is_active', 'category', 'company')
search_fields = ('name', 'description', 'company__company_name')
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(company__user=request.user)
@admin.register(ServiceRequest)
class ServiceRequestAdmin(admin.ModelAdmin):
list_display = ('service', 'customer', 'status', 'created_at')
list_filter = ('status', 'created_at')
date_hierarchy = 'created_at'
10. Middleware for Request Tracking
from .models import Service, ServiceView
from django.utils import timezone
class ServiceViewMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Check if this is a service detail page
path_parts = request.path.strip('/').split('/')
if len(path_parts) >= 3 and path_parts[0] == 'services' and path_parts[1] == 'detail':
try:
service_id = int(path_parts[2])
service = Service.objects.get(id=service_id)
# Record anonymous view
ServiceView.objects.create(
service=service,
ip_address=self.get_client_ip(request),
user=request.user if request.user.is_authenticated else None,
viewed_at=timezone.now()
)
except (ValueError, Service.DoesNotExist):
pass
return response
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
Don't forget to add it to your settings:
MIDDLEWARE = [
# ... other middleware
'services.middleware.ServiceViewMiddleware',
]
Conclusion
Building Netfix with Django taught me how powerful and flexible the framework truly is. These advanced features - from custom user models to complex queries with annotations - enabled me to create a robust marketplace with clean, maintainable code.
What I appreciate most about Django is how it scales with your knowledge. As you discover more features, you can refactor and improve your application while maintaining backward compatibility