skip to content

When splitting a Django clinic-booking monolith into apps, how do you decide the boundaries, and when should an app become reusable?

level: principalimportance: should knowfreq 24%

answer

  1. cohesion by domain, not by layer
  2. dependency direction between models
  3. migrations make moves expensive
  4. reusable means no project imports

basics

~20 s

Draw Django app boundaries around cohesive domain areas whose models change together, keep cross-app dependencies one-directional, and decide early because labels and migrations make moving models costly. Make an app reusable only when a second project needs it.

solid answer

~40 s

Start from the domain: in a clinic-booking system, `patients`, `scheduling`, `billing` and `notifications` are candidate apps because each owns models that change together and can be described in one sentence. Avoid layer-based apps (`models`, `views`) and a catch-all `core`. Keep dependencies acyclic — `scheduling` may reference `patients.Patient`, but `patients` should not import `scheduling` — using string references like `"patients.Patient"` for foreign keys. Decide early, because an app's label is baked into tables, migrations, content types and permissions, so moving models later is a migration project. Put a custom user model in its own app before the first migration. An app becomes **reusable** only when it has a second consumer: it must avoid importing project modules, namespace its templates and static files, ship its migrations and set `default_auto_field` explicitly.

go deeper

for a junior

Know that apps should group related features, and that a custom user model belongs in its own app from the start.

for a middle

Explain dependency direction between apps and string references such as patients.Patient for cross-app foreign keys.

for a senior

Weigh the cost of moving models later, labels, migrations, content types, and plan boundaries to avoid it on a growing codebase.

for a principal

Own the boundary policy: cohesion and ownership criteria, dependency rules, and when an internal app earns extraction as a reusable package.

## Why boundaries matter more in Django than they look Django apps are cheap to create but expensive to reshape. An app's label is persisted in default table names, migration files and history, content types and permission strings. So an app boundary is not just a folder: it is a set of database identifiers and migration graphs that later refactors must carry. There is no single right split; the goal is to make the likely changes cheap. ## Criteria for drawing boundaries - **Cohesion** — models that are created, changed and queried together belong together. `Appointment`, `Slot` and `Practitioner` form a `scheduling` app; `Invoice` and `Payment` form `billing`. - **One-sentence purpose** — if the app's job needs "and" twice, it is probably two apps. - **Dependency direction** — decide which app may know about which. `scheduling` references `patients`; `billing` references `scheduling`; nothing references back. Cycles between models modules cause import tangles and cross-app migration dependencies in both directions. - **Team ownership** — in a large monolith, an app is a natural unit of code ownership and review. - **Change frequency** — isolating volatile integrations (a `notifications` app wrapping SMS and email) keeps churn out of core models. ## Anti-patterns | Anti-pattern | Why it hurts | |---|---| | one giant `core` app | every change touches one migration graph; no ownership | | apps split by layer (`models`, `api`, `views`) | a feature change spans every app; boundaries follow code type, not domain | | an app per model | cross-app foreign keys everywhere, migrations entangled | | mutual imports between apps | load-order fragility and circular migration dependencies | ## Techniques that keep boundaries honest 1. **String model references** — `ForeignKey("patients.Patient", on_delete=models.PROTECT)` avoids importing another app's models module. 2. **Signals or explicit service functions** for reactions across apps, so `patients` need not know `notifications` exists. 3. **The custom user model first** — create an `accounts` app with the user model before the first migration, because swapping `AUTH_USER_MODEL` later is painful. 4. **Explicit `Meta.db_table`** where a model is likely to move, which pins table names (though migrations, content types and permissions still carry the label). ## When an app should become reusable Reuse is a cost, not a default. Package an app for reuse when a **second project** will install it. A reusable app: - imports nothing from the host project and reads configuration from settings with sensible defaults; - namespaces its templates as `templates/<label>/` and static files as `static/<label>/`, so host projects can override deliberately; - ships its migrations and sets `AppConfig.default_auto_field` explicitly, so a host with a different `DEFAULT_AUTO_FIELD` does not generate migrations for it — Django's own contrib apps pin `AutoField` for this reason; - treats its label as public API that never changes. ## Trade-offs to say out loud - Finer apps improve ownership but add cross-app foreign keys and migration dependencies. - Coarser apps are simpler to migrate but blur ownership. - Extracting a reusable app freezes its interface; keep it internal until the second consumer is real. ## What an interviewer is listening for A principal-level answer gives criteria rather than a fixed recipe, names the Django-specific costs (labels, migrations, content types, the user model) and treats reusability as something earned by a second consumer.

  • Why must a reusable app set AppConfig.default_auto_field?
    Its shipped migrations fix the primary key type. If the app leaves it unset, a host project whose `DEFAULT_AUTO_FIELD` differs will see `makemigrations` generate new migrations for the app, which can conflict with the app's future releases. Setting it pins the type regardless of host settings.
  • How do you let one app react to events in another without importing it?
    Have the source app send a signal, or expose a small service function, and let the dependent app subscribe in its own `AppConfig.ready()`. The dependency then points from the reacting app to the source, keeping the source unaware of its consumers.

saying these in an interview costs you the question

  • Splits apps by technical layer such as models, views and api
  • Puts everything in one core app to avoid decisions
  • Assumes models can be moved between apps with a simple rename
  • Makes every app reusable before a second project needs it
  • Lets two apps import each other's models freely