Derived Modular Arch
Concepts

Where to put a file

Code placement algorithm: from composition root to promotion and shared.

The answer to “where does this file go” isn't taste — it's a sequence of predicates. Walk the steps top to bottom and stop at the first match.

Algorithm

  1. Route, layout, wiring, providers? → composition root: app/, pages/, or routes/. Only mount */public/*, no business logic.
  2. Must another module import this code (product flow)?services/<name>/public/. Promotion: move + revisit public API. Never feature → feature.
  3. Portable helper, UI, or type already needed by 2+ modules?shared/{ui,lib,api,model,domain}. Don't extract on first use.
  4. Sole consumer is one module? → colocate inside that module (internal file or ui/, model/ segment).
  5. New leaf flow, mounted only from composition root?features/<name> (stage 0 — one file, stage 1+ — folder with public/).
  6. Edge would create a cycle or step upward? → port in public/ports.ts + bind in composition root. Don't use a port to bypass lawful promotion.

Quick predicates

PathPredicate
app/ / pages/ / routes/Assembles the app; modules don't import it
features/No inbound from other modules
services/Has inbound from modules; folder created on first promotion
shared/Portable code without a product flow

Inbound for promotion — only from features/* and services/*. Mount from composition root does not trigger promotion.

Table: typical file → folder

You have…WhereExample
page.tsx, +page.svelte, index.astrocomposition rootapp/page.tsx
Providers, event wiringcomposition rootapp/providers.tsx
Screen / flow for a routefeatures/<name>/public/ or stage-0 filecatalog-page.tsx
Store for one featurecolocate in featurecheckout.store.ts
Composable for one feature (Vue)colocate in featureuse-catalog-search.ts
Logic for catalog and checkoutservices/<name>/public/cart.ts
Button without product logicshared/ui/button.tsx
formatCurrency, dates, i18nshared/lib/format-currency.ts
Base HTTP client, interceptorsshared/api/http.ts
Endpoints for one featurecolocate in featurecatalog.api.ts
Product, injection keyshared/model/ or shared/domain/product.ts
Unit test for storenext to storecheckout.store.test.ts
E2Eapp roote2e/ next to config

services vs shared (last manual step)

Question: “Could this code live in another product without changing its meaning?”

  • Yesshared/ (formatting, UI primitive, type)
  • Noservices/ (cart, session, catalog API as a product flow)

When in doubt, keep colocation in the feature until a second consumer. More — Services vs Shared.

Cross-module wiring

Feature does not import feature. Options:

TaskMechanism
Callback / propscomposition root passes between public/ entries
Event without subscriberspublic/*.events.ts; subscribe in app/
Cycle / external seedpublic/ports.ts + bind in app/

Breakdown — Cross-module wiring.

What tooling checks

SituationRule
Import upward across layerslayer-direction
features/afeatures/bfeature-to-feature
Import bypassing public/public-api
Feature with inboundfeature-has-inbound
Candidate for shared/servicesshared-candidate (doctor) — help names Placement #2–3 by layer

Full error breakdown — dma check: violation reference.

What's next

On this page