DevHuzaifa's Portfolio
3 min readUpdated Aug 12, 2026

Thin Views, Fat Services

Learn how the Service Layer pattern replaces fat views and messy serializers in Django. Keep your business logic isolated, testable, and reusable across web endpoints, tasks, and CLI workflows

#Django#System Design

What is Business Logic?

Business logic represents the domain-specific rules and workflows that dictate how an application behaves, completely independent of how users access it (HTTP REST API, GraphQL, CLI script, or background task).

Examples of business logic include:

  • Calculating multi-item discounts and applying tax regulations.
  • Verifying inventory availability before placing an order.
  • Executing credit card payments via third-party APIs (e.g., Stripe).
  • Triggering transactional emails, PDF invoice generation, or webhook notifications.

The Pitfalls of Fat Views and Fat Serializers

1. The Problem with "Fat Views"

When views handle input parsing, database mutations, third-party integrations, and error handling simultaneously:

  • Zero Reusability: If a Celery task or management command needs to create an order, you cannot reuse view code without hacking fake HTTP requests.
  • Testing Friction: Testing a core business rule requires constructing full HTTP request objects, setting header states, and parsing JSON responses.

2. The Problem with "Fat Serializers"

In Django REST Framework (DRF), developers often push business operations into Serializer.create() or validate().

  • Coupling Concerns: Serializers are designed for data serialization, deserialization, and field-level validation.
  • Side Effect Hazards: Triggering external payments or sending emails during serializer.save() couples validation mechanics to execution, making nested serializer logic fragile and difficult to maintain.

What is a Service Layer?

A Service Layer introduces an explicit boundary between your entry points (views, tasks, CLI) and database storage (ORM models). It isolates application use cases inside dedicated, plain Python modules.

  • ViewSet / Interface Layer
    • Validates HTTP requests and permission scopes.
    • Returns appropriate HTTP response status codes.
  • Order Service Layer
    • Coordinates payment, inventory, and order creation.
    • Enforces atomic transactional boundaries.
  • Django Models / Database Layer
    • Stores application state and defines database constraints.

Designing Service Functions in Django

While traditional object-oriented designs utilize service classes, stateless, type-hinted Python functions are often cleaner and easier to maintain in Django apps.

services/orders.py
from decimal import Decimal
from django.db import transaction
from django.core.exceptions import ValidationError
from apps.orders.models import Order, OrderItem
from apps.inventory.services import reserve_inventory
from apps.payments.services import process_payment

@transaction.atomic
def order_create(*, user, items_data: list[dict], promo_code: str | None = None) -> Order:
    """Orchestrates order placement, stock allocation, and payment processing."""
    total_amount = Decimal("0.00")
    
    # 1. Domain Validation & Inventory Check
    for item in items_data:
        if not reserve_inventory(product_id=item["product_id"], quantity=item["quantity"]):
            raise ValidationError(f"Product {item['product_id']} is out of stock.")
        total_amount += item["price"] * item["quantity"]

    # 2. State Persistence
    order = Order.objects.create(user=user, total_amount=total_amount, status=Order.Status.PENDING)
    
    for item in items_data:
        OrderItem.objects.create(order=order, **item)

    # 3. Third-Party Side Effects
    process_payment(user=user, amount=total_amount, order_id=order.id)
    
    order.status = Order.Status.PAID
    order.save(update_fields=["status"])
    
    return order

Clean Integration:

views.py
from rest_framework import viewsets, status
from rest_framework.response import Response
from apps.orders.services import order_create
from apps.orders.serializers import OrderInputSerializer, OrderOutputSerializer

class OrderViewSet(viewsets.GenericViewSet):
    def create(self, request):
        serializer = OrderInputSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # Delegate workflow to the service layer
        order = order_create(
            user=request.user,
            items_data=serializer.validated_data["items"],
            promo_code=serializer.validated_data.get("promo_code")
        )

        return Response(OrderOutputSerializer(order).data, status=status.HTTP_201_CREATED)

Database Transactions Inside Services

Database transactions belong in the service layer—not inside views or global middleware. Wrapping service functions with Django’s @transaction.atomic guarantees:

  1. All-or-Nothing Execution: If payment fails after creating the order record, database changes automatically roll back.
  2. Short Transaction Life: Locks open right before domain processing and close immediately after, preventing database connection exhaustion caused by slow HTTP responses.

Testing Services

Testing service logic becomes fast and lightweight because tests bypass the HTTP stack entirely.

tests/test_order_service.py
import pytest
from apps.orders.services import order_create
from apps.orders.models import Order

@pytest.mark.django_db
def test_order_create_success(user_factory, product_factory):
    user = user_factory()
    product = product_factory(price=100, stock=5)
    
    items = [{"product_id": product.id, "quantity": 1, "price": product.price}]
    order = order_create(user=user, items_data=items)

    assert order.status == Order.Status.PAID
    assert order.total_amount == 100
    assert Order.objects.filter(id=order.id).exists()

When is a Service Layer Unnecessary?

Indirection carries a cost. You can skip the service layer for:

  • Standard CRUD: Views that simply list, fetch, or update standard model fields without side effects.
  • Simple Admin Interfaces: Internal Django admin tools with basic data entry requirements.
  • Early Prototypes: MVPs where architectural agility outweighs multi-system orchestration.
Django Service Layer: Keep Business Logic Out of Views | Articles | Muhammad Huzaifa