Derived Modular Arch
Concepts

Code evolution

How architecture grows without rewriting: second-use, promotion, doctor signals.

DMA doesn't scale by replacing architecture with another. The project densifies in place: folders appear when predicates start carrying information.

You don't create an empty services/ “for growth.” You don't extract to shared/ “just in case.” Growth responds to a second consumer or a tooling signal.

Three project phases

1. Start

src/
├── app/          # or pages/, routes/
├── features/
└── shared/       # almost empty

No services/. All modules are leaf features.

2. First promotion

A module is needed by another module → create services/, move the module, revisit public API. From this point, cycle checks inside services become useful.

Example: catalog calls addToCart, checkout too — cart moves from feature to services/cart/.

3. Domains (when services has grown)

Dense services subgraph or multiple teams → horizontal split (domains/billing/, separate packages). No new vertical layers are added.

Second-use rule

What happenedAction
Feature needed by another modulePromotion into services/
Portable helper/UI needed by 2+ modulesLift into shared/ (appropriate group)
Type from public/ needed by 2+ modulesMove to shared/domain

import type is also a graph edge. Bypass via types won't work.

Mount from composition root does not count as inbound and does not trigger promotion.

Promotion — a design moment

When a feature gets inbound from another module, an API grown for one consumer becomes a contract for many. This is a deliberate step:

  1. Move to services/<name>/
  2. Revisit public/ — what's actually needed outside
  3. Confirm dma check is green (feature-has-inbound no longer complains)

Module growth triggers

SignalActionTooling
Sibling files with shared basename prefix at stage 0 (checkout.tsx + checkout.store.ts)Stage 1: folder + public/stage-growth
~8+ internal filesStage 2: ui/model/api segmentsstage-growth
Bloated public/Stage 3: split moduleReview (no doctor yet)
Feature needed by a modulePromotion → services/feature-has-inbound
Cycle between modulesPort + wiring in app, or extract to sharedno-cycle
Dense services subgraphHorizontal splitdense-services

Thresholds are CLI defaults. v1 has no user config to change them.

dma doctor signals

Soft hints. Exit code 0 — doesn't break CI.

shared-candidate

A module file is imported by 2+ other modules. Often this is a healthy services/*/public/* surface. Before a lift into shared/, ask: is this product logic or a portable helper?

stage-growth

Module structure lags size: a file module gained sibling files with the same basename prefix, or a folder has many files without ui/ / model/ / api/ segments.

dense-services

The services/ subgraph looks dense — time to split by domain.

orphan-public

A file in public/ nobody imports — dead contract. Remove or make internal.

More on the command — dma doctor.

When adopting DMA in an existing project, promotion and doctor signals go together with a phased plan — Migration.

What's next

On this page