Code Organization
Code Organization — File Modularity
Multiple developers and AI agents work on separate issues in parallel. To minimise merge conflicts and keep pull requests small and reviewable, the architecture enforces a strict "one concern per file" rule across every layer.
General Principles
One component / function / model per file
Each React component, FastAPI router, Pydantic model, durable job, and utility function lives in its own file. Prefer many small files over a few large ones.
Barrel files for public API only
index.ts (TypeScript) and __init__.py (Python) files re-export the module's public surface. Implementation stays in dedicated files beside the index. This means two developers can add new items to the same module without touching the same lines.
Co-locate tests appropriately
- TypeScript/React: Test files live next to source (
Button.tsx→Button.test.tsx). - Python: Tests go in a
tests/directory with filenames matching source modules.
Flat over nested
Two directory levels deep is fine. Five hinders discoverability. Avoid unnecessary nesting.
Frontend — ui/web-app & ui/components
ui/web-app is the applications UI; ui/components is a shared component/library workspace (one of the ui/* shared packages — see Monorepo) that houses reusable presentational components consumed by ui/web-app. The same one-concern-per-file rule applies to both.
| ✅ Prefer | ❌ Avoid |
|---|---|
components/Button.tsx — one component per file | components/index.tsx containing Button, Card, Modal |
components/Button.test.tsx — co-located test | __tests__/components.test.tsx testing many components |
hooks/useAuth.ts — one hook per file | hooks/index.ts with multiple hook definitions |
Re-export from barrel files:
// components/index.ts
export { Button } from "./Button";
export { Card } from "./Card";Backend — backend/api (HTTP) & backend/worker (durable)
backend/api is the FastAPI HTTP service; backend/worker holds the Restate durable-execution handlers. The same one-concern-per-file rule applies to both.
| ✅ Prefer | ❌ Avoid |
|---|---|
routers/health.py, routers/patients.py (backend/api) | routers.py with all route handlers |
models/patient.py, models/appointment.py | models.py with every Pydantic model |
jobs/sync.py, jobs/notify.py (backend/worker) | jobs.py with all durable handlers |
Schema — schema/ (Drizzle)
Tables, RLS, and migrations live in the top-level schema/ package — the single source of truth that Drizzle Kit owns and the Python api reflects (no Alembic). One concern per file:
schema/
├── src/
│ ├── tables/ # one Drizzle table (concern) per file
│ │ ├── patient.ts
│ │ └── appointment.ts
│ └── rls.ts # crudPolicy / authenticatedRole RLS definitions
├── drizzle.config.ts # Drizzle Kit config
└── migrations/ # generated SQL — one migration history
| ✅ Prefer | ❌ Avoid |
|---|---|
tables/patient.ts, tables/appointment.ts — one table per file | tables.ts / schema.ts with every table |
Generated SQL committed under migrations/ | Hand-edited or duplicated migration histories |
Authorization policies — policies/ (Cerbos)
Cerbos policy-as-code lives in a version-controlled, top-level policies/ directory (Cerbos YAML), loaded by the self-hosted Cerbos PDP (see Backend). One resource per file:
| ✅ Prefer | ❌ Avoid |
|---|---|
policies/patient.yaml, policies/appointment.yaml — one resource policy per file | policies.yaml with every resource's rules |
Infrastructure & Config
- One Pulumi resource group per file in
infra/(e.g.,gcp.ts,neon.ts,cloudflare.ts). - One dbt model per
.sqlfile (dbt enforces this). - One GitHub Actions workflow per
.ymlfile.
Why This Matters for AI-Assisted Development
When AI coding agents (Copilot, Copilot Workspace, or third-party agents) work on issues concurrently, each agent typically modifies a small set of files. If unrelated features share a file, the agents' branches produce conflicting diffs on the same file, requiring manual resolution.
Granular files mean:
- Zero conflicts. Two agents adding two different components create two different files.
- Smaller PRs. Each change touches only the files it needs to.
- Easier reviews. Human and automated reviewers see focused, relevant diffs.
- Trivial reverts. Removing a feature is as simple as deleting its file(s).
When in doubt, split it out.