Structuring a project as it grows
A Django project that starts as two apps and a hundred lines is easy to navigate. The same project at fifty thousand lines, if it grew without thought, is a maze. Structure is not something you get right once at the start — it is a set of decisions you revisit as the project grows, and knowing when to split, extract and reorganise is what keeps a codebase workable for years. This lesson is the practical guidance on structuring a Django project as it scales, closing the architecture module.
Start simple, split when it hurts
The first rule is a caution against the opposite mistake — over-structuring early:
- A small project can be one or two apps. Do not carve it into fifteen tiny apps on day one because a blog post said microservices; you will spend more time wiring than building.
- Split an app when it hurts — when it has grown to own several distinct concerns and changing one thing means scrolling past three unrelated ones. The signal to split is felt, not scheduled.
This mirrors the whole module's theme: structure serves the project's actual needs, not a diagram. The
apps-and-structure lesson said "divide by feature, not by layer"; this lesson is about when to make those
divisions as the project grows. Nidaan might start as patients and appointments and later grow
reports, billing, and accounts as each becomes substantial.
Divide by feature, and keep apps cohesive
The enduring principle (from module 1, worth restating at scale): an app should own one coherent slice of the domain — its models, views, URLs, templates, and tests together. As the project grows, the test of a good app boundary is:
- High cohesion — everything in the app is about one thing (everything in
billingis about billing). - Low coupling — apps depend on each other as little as possible, through clear interfaces (a model, a service function), not by reaching into each other's internals.
When an app starts doing two unrelated jobs, split it. When two apps are so entangled they always change together, perhaps they are really one. The goal is that a developer can be told "that's a billing concern" and know which app to open — and change billing without touching patients.
Where non-model, non-view code goes
As a project grows it accumulates code that is neither a model nor a view, and giving it a home keeps apps tidy. The conventional places within an app:
billing/
models.py
views.py
urls.py
admin.py
services.py # multi-model operations (the service-layer lesson)
selectors.py # complex read queries, if you separate reads from writes
managers.py # custom managers/querysets (if large)
tasks.py # Celery tasks (the background-work lesson)
signals.py # signal receivers (connected in apps.py ready())
forms.py
serializers.py # DRF, if this app has an API
tests/ # a package of tests, split by kind
None of these are mandated by Django — they are conventions the community converged on so that, in any
project, you know where to look for a service, a task, or a form. The point is not the exact filenames but
that each kind of code has a predictable home, so the app does not become a views.py with three
thousand lines doing everything.
Settings, and the shared-code question
Two project-wide structure decisions:
- Settings grow with the project — the settings-and-secrets lesson's split (
base/development/production) becomes worthwhile once environments diverge structurally. Small projects keep one env-driven file; large ones split. - Shared code that several apps use — common utilities, base model classes (an abstract
TimestampedModelwithcreated_at/updated_at), shared mixins — goes in acoreorcommonapp that others may depend on. The rule: dependencies flow towardscore(many apps depend oncore;coredepends on none of them), never in a cycle. A dependency cycle between apps is a structural smell to resolve.
Keep the dependency direction clear: feature apps may depend on core and, sparingly, on each other through
defined interfaces — but never in a loop.
Refactor structure deliberately, as a real activity
The final and most important point: structure is maintained, not set once. As a project grows you will periodically need to:
- Split an app that has outgrown its boundary (extract
billingout ofappointments). - Extract a service when a model method has become a cross-cutting process (the service-layer lesson).
- Move shared code to
corewhen a third app needs what two others already have. - Reorganise when the same "where does this go?" question keeps arising — that recurring uncertainty is the codebase telling you its structure no longer fits.
Do these as deliberate refactors — with tests as your safety net (the whole point of the testing module) — not as heroic rewrites. A project stays healthy not because someone designed it perfectly at the start (nobody can), but because the team keeps its structure honest as requirements change: splitting what grew too big, extracting what became cross-cutting, and resisting both premature structure and neglected structure. That ongoing judgement — start simple, divide by cohesion, give each kind of code a home, keep dependencies acyclic, and refactor structure as a real activity — is what separates a codebase people can work in for years from one everyone dreads. It is also the closing lesson of this module: patterns and structure serve the project, and the skill is applying them when the project actually needs them.
Check your work
Start simple. One or two apps for a small project; split an app only when it hurts (owns several concerns) — do not over-structure early.
Divide by feature, cohesive and loosely coupled. An app owns one coherent domain slice (models, views, URLs, tests together); high cohesion, low coupling; split when an app does two jobs, merge when two always change together.
A home for each kind of code. services.py, selectors.py, managers.py, tasks.py, signals.py,
forms.py, serializers.py, tests/ — conventional homes so code is findable, not piled into views.py.
Project-wide. Split settings when environments diverge structurally; shared code in a core app that
others depend on, with dependencies flowing towards core and never in a cycle.
Structure is maintained. Split, extract, move to core, and reorganise as deliberate refactors (tests
as the safety net) when the project outgrows its shape — resisting both premature and neglected structure.
Practice
- Look at Nidaan's apps and, for each, state the one concern it owns; find any app doing two jobs and propose a split.
- Take an app with everything in
views.pyandmodels.py; move services, tasks and forms into their conventional files. - Identify code duplicated across two apps and extract it into a
coreapp; confirm the dependency flows towardscore. - Find (or imagine) a dependency cycle between two apps and propose how to break it.
- Decide whether Nidaan warrants split settings yet, and justify the answer against how its environments differ.
- Describe a structural refactor you would do only once the project grew — and why doing it on day one would be premature.
Official documentation
- Django — Applications — Apps and their configuration.
- Django — Reusable apps — Cohesive, self-contained app design.
- Django — Design philosophies — Loose coupling and cohesion as guiding principles.
Next: the capstone brief — a clinic and diagnostic-lab manager.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship