Home Interview Django Interview Questions Answered — ORM, Views, Middleware & More

Django Interview Questions Answered — ORM, Views, Middleware & More

In Plain English 🔥
Think of Django like a fully-equipped restaurant kitchen. The menu is your URL routing, the chefs are your views, the pantry is your database accessed through the ORM, and the health inspector rules are your middleware. An interviewer isn't just checking if you know the kitchen exists — they want to know if you can run a dinner service under pressure. These questions reveal whether you understand how the whole kitchen works together, not just how to boil water.
⚡ Quick Answer
Think of Django like a fully-equipped restaurant kitchen. The menu is your URL routing, the chefs are your views, the pantry is your database accessed through the ORM, and the health inspector rules are your middleware. An interviewer isn't just checking if you know the kitchen exists — they want to know if you can run a dinner service under pressure. These questions reveal whether you understand how the whole kitchen works together, not just how to boil water.

Django powers some of the most-visited sites on the planet — Instagram started on it, Pinterest scaled with it, and thousands of startups ship with it every year. When a company posts a Django backend role, they're not looking for someone who memorised the docs. They want engineers who understand the framework's design decisions deeply enough to bend them when business requirements get weird. That's what separates a candidate who gets an offer from one who gets a polite rejection email.

The problem with most Django interview prep is that it's surface-level. Lists of questions with one-line answers that don't explain why Django works the way it does. That leaves you vulnerable the moment an interviewer asks a follow-up — 'okay, but why would you choose that approach?' — and you freeze. Real interviews dig into trade-offs, failure modes, and architectural judgment, not just API recall.

By the end of this article you'll be able to explain Django's MVT architecture, the ORM query lifecycle, middleware execution order, signals vs direct calls, and caching strategies — each with the real-world context that makes your answers land. You'll also know the three mistakes that silently kill otherwise good Django interviews.

Django's MVT Architecture — Why It's Not Quite MVC

Almost every Django interview starts here, and almost every candidate fumbles it by saying 'Django uses MVC.' It doesn't — not exactly. Django uses MVT: Model, View, Template. The naming shift isn't cosmetic; it reflects a genuine architectural difference worth understanding.

In classic MVC, the Controller handles HTTP requests, decides what data to fetch, and tells the View what to render. In Django, that controller logic lives in the View function or class. Django's Template is purely the presentation layer — it has no business logic, and the framework enforces that through the deliberately limited template language. The 'Controller' in the traditional sense is Django's URL dispatcher itself, which routes the incoming request to the right view.

Why does this matter in an interview? Because understanding MVT shows you understand Django's philosophy: keep business logic in Python (views and models), keep presentation in templates, and trust the framework to wire them together. When you understand that philosophy, you make better decisions — like knowing that fat models and thin views is a Django best practice, not just a style preference.

Interviewers love asking 'where would you put business logic in a Django app?' The wrong answer is 'in the template.' The right answer is 'in the model or a service layer, with the view acting as a thin coordinator.'

mvt_example.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637383940414243
# ─── models.py ───────────────────────────────────────────────────────────────
# The Model owns the data AND the business rules around that data.
# Notice: the discount calculation lives HERE, not in the view or template.
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    base_price = models.DecimalField(max_digits=8, decimal_places=2)
    is_on_sale = models.BooleanField(default=False)

    def discounted_price(self):
        """Business logic belongs in the model — fat models, thin views."""
        if self.is_on_sale:
            return self.base_price * 0.80  # 20% discount
        return self.base_price

    def __str__(self):
        return f"{self.name} (£{self.discounted_price():.2f})"


# ─── views.py ────────────────────────────────────────────────────────────────
# The View is a thin coordinator: fetch data, pass to template. Nothing more.
from django.shortcuts import render
from .models import Product

def product_list(request):
    # QuerySet is lazy — no DB hit yet, just a description of the query
    on_sale_products = Product.objects.filter(is_on_sale=True)

    # DB hit happens HERE when Django evaluates the QuerySet for the template
    context = {
        'products': on_sale_products,  # passed to the template by name
    }
    return render(request, 'shop/product_list.html', context)


# ─── shop/product_list.html (Template) ───────────────────────────────────────
# Templates are dumb on purpose — only presentation, no business logic.
# {% for product in products %}
#     <p>{{ product.name }} — {{ product.discounted_price }}</p>
# {% empty %}
#     <p>No sales on right now.</p>
# {% endfor %}
▶ Output
# No runnable terminal output here — this is a web framework.
# When a browser hits /products/, Django:
# 1. Matches URL → calls product_list(request)
# 2. Builds QuerySet for on-sale products
# 3. Evaluates QuerySet → SQL: SELECT * FROM shop_product WHERE is_on_sale=True
# 4. Renders template with product data
# 5. Returns HTTP 200 with HTML body to the browser
⚠️
Interview Gold:When asked 'what's the difference between MVC and MVT?', say: 'In Django's MVT, the URL dispatcher plays the role of the Controller, routing requests to Views. Django's Views are closer to MVC Controllers — they coordinate data fetching and template rendering. The Template is strictly presentational, which is enforced by the template language's intentional limitations.' That answer shows architectural understanding, not just terminology recall.

Django ORM Deep Dive — QuerySets, Lazy Evaluation & the N+1 Problem

The ORM is where most Django interviews separate candidates. Everyone knows .filter() and .all(). The real question is whether you understand when the database is actually hit — because getting that wrong kills performance in production.

Django QuerySets are lazy. When you write Product.objects.filter(is_on_sale=True), nothing touches the database. Django builds a description of the query in memory. The database is only hit when you iterate, slice, call list(), or access len() on the QuerySet. This design lets you chain filters efficiently without redundant round-trips.

The N+1 problem is the most common ORM performance trap — and interviewers know it. It happens when you load a list of objects and then access a related object on each one inside a loop. That's one query to get the list, then N more queries for each related record. On a list of 500 orders, you've just fired 501 database queries instead of 2.

The fix is select_related() for ForeignKey/OneToOne relationships (SQL JOIN) and prefetch_related() for ManyToMany or reverse ForeignKey relationships (separate optimised query). Knowing which one to use — and why they work differently under the hood — is what makes you stand out.

orm_queryset_patterns.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
from django.db import models

# ─── models.py ───────────────────────────────────────────────────────────────
class Author(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE,
                               related_name='books')
    published_year = models.IntegerField()

    def __str__(self):
        return self.title


# ─── views.py ────────────────────────────────────────────────────────────────
from django.shortcuts import render
from .models import Book

# ❌ THE N+1 PROBLEM — fires 1 + N database queries
def book_list_slow(request):
    books = Book.objects.all()        # Query 1: SELECT * FROM book
    for book in books:
        # Each access to book.author fires a NEW query:
        # Query 2: SELECT * FROM author WHERE id=1
        # Query 3: SELECT * FROM author WHERE id=2  ... and so on
        print(book.author.name)       # 💥 N additional queries
    return render(request, 'books/list.html', {'books': books})


# ✅ THE FIX — select_related() for ForeignKey (SQL JOIN, 1 query total)
def book_list_fast(request):
    # Django generates: SELECT book.*, author.* FROM book
    #                   INNER JOIN author ON book.author_id = author.id
    books = Book.objects.select_related('author').all()

    for book in books:
        # author data is already in memory — ZERO extra queries here
        print(book.author.name)       # ✅ No additional DB hit

    return render(request, 'books/list.html', {'books': books})


# ✅ prefetch_related() for reverse FK / ManyToMany (2 queries, not N+1)
def author_list_with_books(request):
    # Query 1: SELECT * FROM author
    # Query 2: SELECT * FROM book WHERE author_id IN (1, 2, 3, ...)
    # Django then stitches the results together in Python memory
    authors = Author.objects.prefetch_related('books').all()

    for author in authors:
        # author.books.all() uses the prefetched cache — no extra query
        book_titles = [book.title for book in author.books.all()]
        print(f"{author.name}: {book_titles}")

    return render(request, 'books/authors.html', {'authors': authors})


# ─── LAZY EVALUATION DEMO ────────────────────────────────────────────────────
def queryset_laziness_demo():
    # Step 1: No DB hit — just builds a query object
    recent_books = Book.objects.filter(published_year__gte=2020)

    # Step 2: Still no DB hit — chaining is free
    recent_books = recent_books.order_by('-published_year')

    # Step 3: DB hit happens HERE — list() forces evaluation
    book_list = list(recent_books)
    # SQL fired: SELECT * FROM book WHERE published_year >= 2020
    #            ORDER BY published_year DESC

    print(f"Fetched {len(book_list)} books with a single query")
    return book_list
▶ Output
# book_list_slow() with 100 books:
# Fired 101 SQL queries — 1 for books, 100 for authors

# book_list_fast() with 100 books:
# Fired 1 SQL query — JOIN fetches everything at once

# author_list_with_books() with 20 authors, 100 books:
# Fired 2 SQL queries total — always 2, regardless of row count
# J.K. Rowling: ['Harry Potter', 'The Ickabog']
# George Orwell: ['1984', 'Animal Farm']

# queryset_laziness_demo():
# Fetched 34 books with a single query
⚠️
Watch Out:Using `select_related()` on a ManyToMany field won't work — it silently falls back to N+1 behaviour without raising an error. Always use `prefetch_related()` for M2M and reverse ForeignKey. A quick way to catch N+1 in development: install `django-debug-toolbar` and watch the SQL panel — it highlights duplicate queries in red.

Middleware — Django's Request/Response Pipeline Explained

Middleware is one of those Django concepts that interviewers love because it reveals whether you understand the framework's internals or just its surface API. Most candidates know middleware exists for authentication and CSRF. Fewer can explain the execution order, and almost none can write custom middleware correctly on the spot.

Think of middleware as a stack of airport security layers. Your request passes through each layer on the way in, reaches the view (the gate), and then passes back through each layer in reverse on the way out. Django's MIDDLEWARE setting in settings.py defines this stack — top to bottom for requests, bottom to top for responses.

This bidirectional flow matters. SecurityMiddleware sits at the top intentionally — it enforces HTTPS redirects before any other processing happens. SessionMiddleware must come before AuthenticationMiddleware because auth needs the session to be set up first. Swap them and you get a cryptic AttributeError at runtime.

Custom middleware is powerful for cross-cutting concerns: request timing, API key validation, per-request feature flags, or audit logging. The modern Django middleware pattern uses a callable class — understanding both the old process_request/process_response style and the new callable style shows real depth.

custom_middleware.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
# ─── middleware.py ───────────────────────────────────────────────────────────
import time
import logging
from django.http import JsonResponse

logger = logging.getLogger(__name__)


class RequestTimingMiddleware:
    """
    Logs how long each request takes and adds an X-Response-Time header.
    This is the MODERN Django middleware pattern (callable class).
    """

    def __init__(self, get_response):
        # Called ONCE when the server starts — good place for one-time setup
        self.get_response = get_response
        logger.info("RequestTimingMiddleware initialised")

    def __call__(self, request):
        # ── INBOUND: code here runs BEFORE the view ──────────────────────────
        request_start_time = time.monotonic()  # monotonic = immune to clock drift

        # Hand off to the next middleware / view in the chain
        response = self.get_response(request)

        # ── OUTBOUND: code here runs AFTER the view ──────────────────────────
        elapsed_ms = (time.monotonic() - request_start_time) * 1000

        # Add timing info to every response header — useful for API clients
        response['X-Response-Time'] = f"{elapsed_ms:.2f}ms"

        logger.info(
            f"{request.method} {request.path} completed in {elapsed_ms:.2f}ms"
        )

        return response


class ApiKeyMiddleware:
    """
    Validates API key for /api/ routes only.
    Demonstrates short-circuiting — returning early WITHOUT calling the view.
    """
    VALID_API_KEYS = {'prod-key-abc123', 'staging-key-xyz789'}  # Use DB in real life

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Only enforce on /api/ routes — leave admin and frontend alone
        if request.path.startswith('/api/'):
            api_key = request.headers.get('X-API-Key', '')

            if api_key not in self.VALID_API_KEYS:
                # Short-circuit: return 401 WITHOUT calling the view at all
                return JsonResponse(
                    {'error': 'Invalid or missing API key'},
                    status=401
                )

        # Key is valid (or not an API route) — proceed normally
        response = self.get_response(request)
        return response


# ─── settings.py ─────────────────────────────────────────────────────────────
MIDDLEWARE = [
    # Execution order: top → bottom on request, bottom → top on response
    'django.middleware.security.SecurityMiddleware',      # Layer 1 in/out
    'django.contrib.sessions.middleware.SessionMiddleware',  # Layer 2 in/out
    'django.middleware.common.CommonMiddleware',          # Layer 3 in/out
    'django.middleware.csrf.CsrfViewMiddleware',          # Layer 4 in/out
    'django.contrib.auth.middleware.AuthenticationMiddleware',  # Layer 5 in/out
    'myapp.middleware.RequestTimingMiddleware',           # Layer 6 in/out (custom)
    'myapp.middleware.ApiKeyMiddleware',                  # Layer 7 in/out (custom)
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
▶ Output
# Server logs after a POST to /api/products/ with a valid key:
# INFO RequestTimingMiddleware initialised ← (once, on server start)
# INFO POST /api/products/ completed in 42.18ms
# Response header: X-Response-Time: 42.18ms

# Server logs after a GET to /api/orders/ with NO key:
# HTTP 401 returned immediately — view function never called
# Response body: {"error": "Invalid or missing API key"}
🔥
Pro Tip:The `__init__` method receives `get_response` — a callable representing the rest of the middleware stack plus the view. The middleware doesn't know or care what's below it. This is the Chain of Responsibility design pattern in action. Mentioning design patterns in a Django interview immediately signals senior-level thinking.

Django Signals vs Direct Calls — When Decoupling Costs You

Signals are one of Django's most misunderstood features, and they're a favourite interview topic because they expose how a candidate thinks about system design trade-offs — not just Django syntax.

Django signals implement the Observer pattern. When something happens — a model is saved, a user logs in, a request finishes — Django broadcasts a signal. Any code that has 'subscribed' to that signal runs automatically. The appeal is decoupling: the model that fires post_save doesn't need to know about the email sender, the audit logger, or the cache invalidator. They all subscribe independently.

But here's where interviewers trip people up: signals are not free. They make code flow implicit and hard to trace. When a User is saved and five signal handlers fire across three different apps, a new team member reading the save code has no idea any of that happens. That's a maintenance burden. Signals also make unit testing harder — you have to remember to disconnect them or use @override_settings.

The honest answer on when to use signals: use them for genuinely cross-app concerns where you don't want the sender to know about the receiver — like a third-party app hooking into your User model's save lifecycle. For same-app logic, a direct method call or a service layer function is almost always clearer and easier to test.

signals_vs_direct.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
# ─── models.py ───────────────────────────────────────────────────────────────
from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    """Extended user data — classic use case for post_save signal."""
    user = models.OneToOneField(User, on_delete=models.CASCADE,
                                related_name='profile')
    bio = models.TextField(blank=True)
    avatar_url = models.URLField(blank=True)

    def __str__(self):
        return f"Profile({self.user.username})"


# ─── signals.py ──────────────────────────────────────────────────────────────
# ✅ GOOD signal use case: UserProfile lives in a different app to User.
# We can't (shouldn't) modify Django's built-in User.save() directly.
# A signal lets us react to User creation without touching the User model.
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import UserProfile

@receiver(post_save, sender=User)
def create_user_profile_on_registration(sender, instance, created, **kwargs):
    """
    Fires automatically after EVERY User.save().
    'created' is True only when a NEW row was inserted — not on updates.
    """
    if created:
        # Only create the profile once, when the User is first created
        UserProfile.objects.create(user=instance)
        print(f"✅ Profile auto-created for new user: {instance.username}")


# ─── apps.py ─────────────────────────────────────────────────────────────────
# CRITICAL: signals must be imported so Django discovers the @receiver decorator
from django.apps import AppConfig

class AccountsConfig(AppConfig):
    name = 'accounts'

    def ready(self):
        # This import triggers the @receiver decorators to register themselves
        import accounts.signals  # noqa: F401 — import for side effects


# ─── CONTRAST: direct call (better for same-app logic) ───────────────────────
# ❌ DON'T use a signal for this — it's all within the same app
# and the logic is tightly coupled to the view anyway.

# BAD: signal-based order confirmation email (same app, obscure flow)
# @receiver(post_save, sender=Order)
# def send_order_email(sender, instance, created, **kwargs):
#     if created:
#         send_email(instance.customer_email, 'Your order is confirmed')

# ✅ GOOD: explicit call in the view — the flow is readable and testable
from django.shortcuts import redirect
from .models import Order
from .email_service import send_order_confirmation_email

def checkout_complete(request):
    order = Order.objects.create(
        customer=request.user,
        total=request.session.get('cart_total', 0)
    )
    # Explicit call — any developer reading this immediately knows what happens
    send_order_confirmation_email(order)
    return redirect('order_detail', pk=order.pk)
▶ Output
# When a new user registers (User.save() with INSERT):
# ✅ Profile auto-created for new user: sarah_jones

# When an existing user updates their email (User.save() with UPDATE):
# (no output — the 'if created' guard prevents duplicate profile creation)

# Django shell demo:
# >>> from django.contrib.auth.models import User
# >>> u = User.objects.create_user('test_user', password='secret123')
# ✅ Profile auto-created for new user: test_user
# >>> u.profile
# <UserProfile: Profile(test_user)>
⚠️
Watch Out:If you forget the `ready()` method in `apps.py` to import your signals, your `@receiver` decorators never register — and the signal fires with no handlers attached. No error is raised. The code just silently doesn't work. This is one of the most common Django bugs in production and a favourite 'gotcha' interview question.
Aspectselect_related()prefetch_related()
Relationship typeForeignKey, OneToOneFieldManyToManyField, reverse ForeignKey
SQL strategySingle JOIN queryTwo separate queries + Python join
Number of queriesAlways 1Always 2 (regardless of row count)
Best forAccessing a parent object from a childAccessing many children from a parent
Memory usageLower — one result setHigher — two result sets merged in Python
Can be chained with filter()Yes — filters apply to JOINYes — with Prefetch() object for fine control
Works across database shardsNo — JOIN requires same DBYes — queries can hit different DBs

🎯 Key Takeaways

  • Django's MVT puts the 'Controller' role in the URL dispatcher — Views are coordinators, not controllers. Business logic belongs in fat models or service layers, never in templates.
  • QuerySets are lazy — the database is only hit when you iterate, slice, or call list(). Chain filters freely; use select_related() for FK joins and prefetch_related() for M2M/reverse FK to kill N+1 queries.
  • Middleware executes top-to-bottom on requests and bottom-to-top on responses. Order in MIDDLEWARE matters — SessionMiddleware must always precede AuthenticationMiddleware or you'll get an AttributeError.
  • Use signals for genuine cross-app decoupling (like hooking into Django's built-in User model). For same-app logic, a direct function call is clearer, easier to test, and easier for future developers to trace.

⚠ Common Mistakes to Avoid

  • Mistake 1: Calling QuerySet methods inside a template tag loop — Symptom: Django Debug Toolbar shows hundreds of identical SQL queries for a single page load — Fix: Move all ORM calls into the view, use select_related()/prefetch_related(), and pass fully evaluated data to the template. Templates should never trigger database queries.
  • Mistake 2: Forgetting to import signals in apps.py ready() — Symptom: Signal handlers silently never fire — no error, no traceback, just missing behaviour in production — Fix: Add import yourapp.signals inside the ready() method of your AppConfig class. The import itself registers the @receiver decorators as a side effect.
  • Mistake 3: Putting business logic in templates using custom template tags — Symptom: Price calculations, discount logic, or access control checks scattered across .html files, impossible to unit test — Fix: Move all logic to model methods or service functions, call them in the view, and pass simple values to the template. Ask yourself: 'Could I test this without a browser?' If no, it's in the wrong place.

Interview Questions on This Topic

  • QExplain what happens internally — step by step — from the moment a browser sends a GET request to your Django app until HTML is returned. Include middleware, URL resolution, the view, and template rendering.
  • QYou've deployed a Django app and the product listing page is loading slowly. Django Debug Toolbar shows 347 SQL queries for a single page load. Walk me through how you'd diagnose and fix this.
  • QA colleague suggests using Django signals to send a confirmation email every time an Order is saved. You disagree. Make the case for and against signals here, and explain what you'd do instead.

Frequently Asked Questions

What is the difference between select_related and prefetch_related in Django?

select_related() uses a SQL JOIN to fetch related ForeignKey or OneToOneField data in a single query — best when accessing a parent from a child object. prefetch_related() fires two separate queries and joins them in Python memory — required for ManyToMany and reverse ForeignKey relationships. Using the wrong one doesn't raise an error; it silently falls back to N+1 behaviour.

How does Django middleware work and what order does it execute in?

Django middleware forms a stack defined in the MIDDLEWARE setting in settings.py. On an incoming request, middleware runs top-to-bottom. After the view produces a response, middleware runs bottom-to-top. Each middleware layer wraps the next using the get_response callable, forming a chain. Order matters critically — for example, AuthenticationMiddleware must come after SessionMiddleware because it reads from the session.

When should I use Django signals and when should I avoid them?

Use signals when you need to react to an event in one app from code in a separate, decoupled app — the canonical example is automatically creating a UserProfile when Django's built-in User is saved. Avoid signals for same-app logic because they make code flow implicit and hard to test. A direct function call or service layer method is almost always the right choice within a single app.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousSpring Boot Interview QuestionsNext →TypeScript Interview Questions
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged