RizTech Academy logo
RizTech Academy
Architecture and Patterns in DjangoLesson 5 of 530 min

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 billing is 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 TimestampedModel with created_at/updated_at), shared mixins — goes in a core or common app that others may depend on. The rule: dependencies flow towards core (many apps depend on core; core depends 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 billing out of appointments).
  • Extract a service when a model method has become a cross-cutting process (the service-layer lesson).
  • Move shared code to core when 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

  1. Look at Nidaan's apps and, for each, state the one concern it owns; find any app doing two jobs and propose a split.
  2. Take an app with everything in views.py and models.py; move services, tasks and forms into their conventional files.
  3. Identify code duplicated across two apps and extract it into a core app; confirm the dependency flows towards core.
  4. Find (or imagine) a dependency cycle between two apps and propose how to break it.
  5. Decide whether Nidaan warrants split settings yet, and justify the answer against how its environments differ.
  6. Describe a structural refactor you would do only once the project grew — and why doing it on day one would be premature.

Official documentation

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