When Your Django App Gets Too Big

The Start

When you first start a Django project you run django-admin startproject myproject and then python manage.py startapp core. You put your first few models in core/models.py. Your first few views go in core/views.py. Everything is in one place. This feels simple and productive. For a while it is.

The problem is that this initial simplicity is a trap. It works so well for small projects that you keep doing it. You add more models to that one file. More views. More URL patterns. Your core app grows from a small command center into a sprawling city with no street signs. You are not building a cohesive application anymore. You are just adding rooms to a house without a plan.

Eventually you find yourself scrolling through a two thousand line models.py file looking for the Product model. You are not sure if the view you need to edit is product_list or list_products. The project that was once a source of joy becomes a source of dread. This is a common story. But there is a simple way out that does not involve rewriting everything or adopting a complex new framework.

The Unit of Organization

The mistake is in how we think about a Django “app”. Newcomers often think their entire project is the app. They build one giant app that holds everything. But the framework’s authors had a different idea. A Django app is meant to be a small self contained component that does one thing well. It represents a single concept or domain in your system.

If you are building an e-commerce platform your project is not the ecommerce app. Your project is the collection of several smaller apps working together. You might have a products app to handle products inventory and categories. You might have an orders app for carts and purchases. You would certainly have a users app for authentication and profiles.

Each of these is a Django app. Each has its own models.py views.py and urls.py. The unit of organization is not the file. It is the app. This is the fundamental insight you need to escape the single app trap. By organizing code around domain concepts you create a mental model that maps directly to your project’s structure.

A Concrete Example

Let’s imagine our e-commerce project has grown unwieldy. The core app has these models all in one models.py file.

# core/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    # user fields...
    pass

class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    # ... and 20 more fields

class Category(models.Model):
    name = models.CharField(max_length=100)
    # ...

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    # ...

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()
    # ...

This is already getting crowded and we have not even added reviews shipping details or payments. The fix is to create new apps for each clear concept.

python manage.py startapp users
python manage.py startapp products
python manage.py startapp orders

Now you move the models into their new homes. The User model goes into users/models.py. The Product and Category models go into products/models.py. The Order and OrderItem models go into orders/models.py.

The relationships between models work exactly the same way across apps. The OrderItem model just needs to import the other models.

# orders/models.py

from django.db import models
# Note the imports from other apps
from users.models import User
from products.models import Product

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    # ...

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()
    # ...

You update INSTALLED_APPS in your settings.py to include the new apps and remove the old core app once everything is moved. This process of refactoring is straightforward. The biggest benefit is the clarity you gain.

A Note on Circular Imports

One problem you might hit when splitting apps is a circular import. For example what if your Product model in the products app needed to know something from the Order model in the orders app? If orders.models imports products.models and products.models imports orders.models Python will raise an error.

Django has a simple solution for this. When defining a ForeignKey or other relationship you can pass the related model as a string instead of a class.

# products/models.py
from django.db import models

class Product(models.Model):
    # ...
    # This avoids a direct import of the Order model
    # The string is 'app_name.ModelName'
    latest_order = models.ForeignKey(
        'orders.Order',
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )

This lazy relationship resolves the model when Django needs it not at import time. This small technique is often all you need to untangle your models.

The Payoff is Clarity

When you structure your project this way the benefits become obvious quickly.

First finding code becomes easy. If there is a bug with how prices are displayed you know to look in the products app. If you need to change the checkout flow you go to the orders app. You do not need to guess or use global search.

Second it scales for teams. One developer can work on adding a new feature to the products app while another fixes a bug in the orders app. They are working in different directories. This means fewer merge conflicts and less stepping on each other’s toes.

Third your code becomes more reusable. You might build a great users app for this project. On your next project you can just copy it over. A well designed Django app is a self contained module.

The goal is to make each app responsible for a single domain concept. If you cannot describe what an app does in one simple sentence it might be doing too much.

This approach does not require any clever tricks or third party libraries. It is just using Django as it was intended. You are aligning with the grain of the framework not fighting against it. This is usually the path to the simplest and most robust solution.

When To Split

So should you start every project with ten different apps? Probably not. That would be premature optimization. Rules about structure are not meant to be applied blindly from day one. They are tools to solve problems.

The time to split your app is when you feel the pain of it being too big. When you get lost in models.py. When your views.py has dozens of imports and functions that have nothing to do with each other. When your urls.py is a confusing mess. The cost of navigating the code has become greater than the cost of organizing it.

This pain is a signal. It is telling you that a concept within your app wants to be its own app. Listen to that signal. The refactoring process is not as scary as it sounds. You can do it incrementally. Maybe you just pull out the users app first. Then a few weeks later you pull out the products app. Each step makes the codebase better. Each step reduces cognitive load for you and your team.

By breaking your large app into smaller more focused apps you regain the simplicity you had at the start of the project. But now it is a durable simplicity that can grow with your application instead of being crushed by it. The larger your application gets the more you will appreciate this structure. It lets you focus on one part of the system at a time which is the only way to manage complexity.

— Rishi Banerjee
September 2025