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

May 13, 2025 - 03:06
 0
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 = 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