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 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.
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.
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.
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.
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