# A — Atomic > **Rule:** one responsibility per module. When a layer file passes ~700 lines, split per UI/data domain using the same prefix. No barrel re-exports. The fourth and final letter of SAMA. *Atomic* is the rule that keeps every other property honest as the codebase grows. *Sorted* doesn't help if every file is 5,000 lines. *Architecture* doesn't help if a single `c21_*.ts` does fifteen unrelated things. *Modeled* doesn't help if the test file balloons to match. ## One responsibility per module The smallest version of the rule: **a module should answer one question.** `c32_session.ts` answers "how do we sign and verify session cookies?" — that's one question. If you find yourself adding "and also generate access tokens" or "and also handle CSRF", you have a second question and the file should split. Concretely: - One model per domain in `c31_*` (`c31_project_config.ts`, `c31_blog.ts`, `c31_games.ts`). - One pure-logic concern per file in `c32_*` (`c32_judge.ts`, `c32_session.ts`). - One I/O target per file in `c14_*` (`c14_github.ts`, `c14_forgejo.ts`). When in doubt: read the filename. If you can't say what's inside in one short clause without `and`, you have an atom problem. ## The ~700-line split rule Lines are a proxy for surface area. Past ~700 lines, even a "single responsibility" file usually contains several sub-domains that each deserve their own atom. The split rule: > When a layer file passes ~700 lines, split per UI/data domain using the same prefix. The prefix stays — both halves are still the same layer. The suffix changes to name the sub-domain. ### c51 split (UI/render layer) ``` c51_render_layout.ts chrome: renderPage, renderNotFound, escape, htmlResponse c51_render_projects.ts /projects body builders c51_render_reports.ts /reports body builders ``` Same layer (c51), three atoms. Layout holds the chrome that everyone reuses; the per-domain files hold body builders that only one part of the site uses. ### c21 split (handlers layer) ``` c21_app.ts routes literal + dispatcher (the "router") c21_handlers_agents.ts /agents/* handlers c21_handlers_auth.ts OAuth handlers c21_handlers_leaderboard.ts /leaderboard handler ``` `c21_app.ts` stays the dispatcher; per-cluster handlers move out the moment the dispatcher would otherwise grow large. ### c13 split (database layer) ``` c13_database.ts connection + dispatcher (when needed) c13_db_runs.ts runs-table SQL c13_db_projects.ts projects-table SQL ``` Same shape: one "core" file, per-domain extensions when growth pushes past the threshold. The number 700 is not magic. It's the line count at which one experienced reader finds it slower to navigate the file than to remember which of three smaller files holds what they want. If 600 is your number, use 600. The rule is *a threshold exists*, not *exactly 700*. ## No barrel re-exports When you split a layer file into multiple atoms, **do not** create an `index.ts` (or `c51_render.ts`, or `c21_handlers.ts`) that re-exports everything. Consumers import directly from the atom they need. ```ts // good — direct from the atom import { renderProjectDetail } from "./c51_render_projects.ts"; import { renderReportTile } from "./c51_render_reports.ts"; // bad — barrel hides the dependency direction import { renderProjectDetail, renderReportTile } from "./c51_render.ts"; ``` Why this matters: - A barrel re-exports everything in the layer, which means importing one helper drags every other helper's transitive deps into your module's compile graph. Bun and TypeScript both pay for this. - The grep that proves *Sorted* relies on direct imports. A barrel makes the grep useless because every cross-file reference goes through the same neutral name. - Renaming or moving a function inside a barrel-ed layer becomes a coordination problem. Without barrels, the IDE follows the import to one obvious place. ## Why ~700 (the rationale) The threshold is set by what a coding agent can hold in a single tight context window without drift. Past about 700 lines, you start needing to cite line numbers in prompts ("look at the function around line 480") which signals the file has outgrown the agent's working memory. Splitting at the threshold keeps context windows small even for non-trivial domains. It also keeps **token cost** bounded. An agent given "edit `c51_render_reports.ts`" pulls in one bounded file plus its sibling test, and not the entire layer. That's the same constraint the [token-saving tips](/blog/2026-05/three-constraints-agentic-coding) push from the prompt side; *Atomic* enforces it from the codebase side. ## Common mistakes - **"It's still one responsibility, just bigger."** Often true at 800 lines, less true at 1,200, and almost never true at 1,800. Re-read the file as if you were new to the codebase. If you'd want a table of contents, you have at least two atoms. - **Splitting along the wrong axis.** Split per UI/data *domain*, not per "type of helper". `c51_render_helpers.ts` + `c51_render_pages.ts` is a worse split than `c51_render_layout.ts` + `c51_render_projects.ts` + `c51_render_reports.ts`. - **Adding a barrel "for ergonomics".** The five extra characters in the import path are not a real cost. The barrel is. - **Splitting too eagerly.** A 200-line file with two clear functions is fine. Don't split before the threshold; the cost of two files is real, even if small. ## What this gives you - Every file fits in one context window with room to spare for its test. - Renames and moves stay local — no barrel to update. - An agent told to "work on the projects domain" reads `c51_render_projects.ts` + `c21_handlers_projects.ts` (if exists) + `c31_project_config.ts` and that's the entire surface. Three small files, one domain, one trip. --- [← M — Modeled](/sama/discipline/modeled) · [/sama](/sama) · [back to the four properties](/sama)