Skip to content

From the foundation template

This page is read live from D:\foundation\docs\ARCHITECTURE.md, the one home of the four layers. Change it there.

Architecture — the four layers ​

From the foundation template (D:\foundation). Change it THERE, then copy it into projects — never edit it only here.

How every project's code is shaped. The folders are the architecture: look at the top of the repo and you see it.

The four layers ​

Every piece of software does four kinds of work. Keep them apart, and let each depend only on the ones below it.

LayerFolderWhat it isExamples
Interfacesinterfaces/How the outside gets in. Screens, pages, buttons, API routes, CLI commands, AI tools (MCP), webhooks, scheduled jobs.a settings page, POST /api/orders, an MCP tool, a nightly job
Featuresfeatures/<name>/What the product does for someone — the verbs a user would name. Each feature has ONE front door, its service.ts, listing those verbs.create a task, assign it, log hours · place an order, refund it · launch a campaign, pause it
Domaindomain/<name>/The rules of the business, and nothing else. Plain logic: data in, decision out. No network, no database.a subtask can't be due after its parent · a discount can't make a price negative · daily spend can't exceed the budget · text on a colour must meet contrast
Infrastructureinfrastructure/<name>/The plumbing to the outside world. Anything that does input or output — and the small shared helpers.the database, file storage, email, payments, sign-in, AI/model APIs, third-party services, the clock

A framework may force one more top-level name (Next.js forces app/ for its routes). It belongs to Interfaces and stays thin.

Domain vs Infrastructure — the pair people mix up ​

  • Infrastructure speaks the vendor's language: SQL rows, HTTP requests, file paths, tokens. It fetches and stores; it does not know what the data means. A database query that returns a row is infrastructure.
  • Domain speaks the business's language: order, invoice, campaign, colour, margin. It decides; it never fetches. Given a row's values, deciding "this order qualifies for free shipping" is domain.

A worked example — showing whether an order ships free:

  1. Infrastructure runs the query and returns { total: 84.00, country: 'US' }. That is all it knows.
  2. Domain applies the rule: US orders over $75 ship free → true.
  3. The feature (orders/service.ts) asks for both and returns the answer.
  4. The interface (the checkout page, or an API, or an AI tool) shows "Free shipping".

Sorting any piece of code ​

Ask, in order:

  1. Is it a way in, or something a person sees? → Interfaces.
  2. Is it something the product DOES for someone? → Features (behind that feature's service.ts).
  3. Is it a RULE of the business? → Domain.
  4. Does it TALK to something outside, or is it a small generic helper? → Infrastructure.

Two checks that settle arguments:

  • "Would this change if we switched vendors?" (Postgres → another database, Stripe → another payments provider, one AI model → another.) Yes → infrastructure.
  • "Would this change if the business rules changed?" Yes → domain (or a feature, if it runs a whole user action).

And one giveaway: domain code can be tested with no network and no database. If a rule can't be tested that way, it is tangled up with the plumbing.

The one rule ​

Dependencies point down: interfaces → features → domain → infrastructure.

  • A screen may call a feature; the database never knows a screen exists.
  • Features never import each other. What two features both need moves DOWN a layer, where the next feature finds it.
  • Nothing loops. Two modules in one layer may be used one way, never both ways round.

Why it pays ​

  • Swap a vendor → only infrastructure changes.
  • Change a rule → it changes in one place.
  • Add a new way in (a mobile app, an AI connector, a public API) → it reuses every feature, because the logic isn't trapped in the old screens. Everything the app can do, the AI can do.
  • Find anything by asking the four questions.

The common mistakes ​

  • Business rules inside screens or routes. Invisible, duplicated, and unreachable from any other interface. The most common one, and the most expensive.
  • Business rules inside database code. Queries that quietly decide things ("only active, non-archived, not overdue").
  • Features calling each other. Two features quietly become one tangled feature.
  • A utils/, common/ or shared/ folder at the top. It becomes a junk drawer. Every shared piece belongs to a specific layer (small helpers: infrastructure/utils/).
  • A second copy "to be safe". Building a separate version of a shared piece instead of changing it. Less work today, two things drifting apart forever.

Changing a shared piece without fear — a parallel change ​

  1. See who depends on it (the health check's --who <file> lists every file that stands on it).
  2. Expand — grow it a new option or slot, so every existing caller keeps working unchanged.
  3. Migrate the callers that need the new behaviour.
  4. Contract — remove the old path once nothing uses it.

All in one task. A copy is never the safe option; it is the expensive one, paid later. (Fowler: "Parallel Change".)

Adding to the stack ​

Rare, deliberate, and never on the side of another change — this is where spaghetti gets in.

  • A new module in an existing layer (a new feature, a new outside service): check what already exists first — most "new" modules already exist. Then register it, so the tools accept its folder.
  • A new layer or a new rule: prove it does not fit — answer the four questions, say why each fails, and what the closest existing place would cost. Most cases end there. If it truly does not fit, record it as an Architecture Decision Record (a dated entry: what changed, why, what else was considered) in the project's own docs/DECISIONS.md, and get the owner's yes.

How it is held ​

A rule written down can be forgotten; a rule the tools enforce cannot. What this template ships today: the methods (.claude/skills/: plan-a-build, prove-it, escape-hunt, ship-it), the handoff cap check (scripts/check-handoff.ps1), the lister of changed test expectations (scripts/test-changes.mjs), and the two script helpers (scripts/args.mjs, scripts/bin.mjs). What a project grows toward — built in the first project that used this, not yet generic here:

  • The system health check (npm run health) — ONE tool, one command, one page: every import judged against the rule; each shared fact found outside its one home ("homes"); copied code; one job under one name; side doors (an interface reaching the database or an outside service around the feature that owns it); and a workaround catalogue — every kind of workaround the project has shown, each a coded detector with real examples it must catch and look-alikes it must not, so what it knows it always finds. Known findings are listed in a baseline that may only shrink; a new one fails. It runs before every push and in the close, and its findings are shown loudest-first where work is planned, so the whole view is in front of whoever writes the code.
  • An edit guard that refuses a new code file outside a registered module of a layer.
  • The close — tests, typecheck, system health, the plan and its proof — and it prints every test expectation the change altered, so a weakened check cannot hide.

Where to learn more ​

  1. Eric Evans, Domain-Driven Design (the "Blue Book"), ch. 4 "Isolating the Domain" — where the four layers come from. Shorter: Vaughn Vernon, Domain-Driven Design Distilled.
  2. Robert C. Martin, "The Clean Architecture" (free, blog.cleancoder.com) — dependencies point inward.
  3. Alistair Cockburn, "Hexagonal Architecture" / "Ports and Adapters" (free) — interfaces and infrastructure as swappable adapters.
  4. John Ousterhout, A Philosophy of Software Design — deep modules: one simple front (service.ts), much hidden.
  5. Neal Ford et al., Building Evolutionary Architectures — fitness functions: checks that keep the shape.