Back to Blog

Language: English

Flattening Features into One Directory Layer Just Makes Them Huge Again

Rather than letting top-level features balloon, nest child features recursively inside their parents and enforce public-surface-only imports by inspecting the import graph with ts-morph in CI.

Organizing code into one directory per feature is a common pattern. But if features are laid out in a single flat layer, big capabilities grow huge all over again.

In our annotation platform, AnnotationSession was exactly that. The annotator opens a task, picks labels, submits, and moves on to the next task. Every piece of code involved in that flow belongs to one cohesive capability called AnnotationSession.

Cramming it into a single level of features makes that one directory balloon. Splitting it instead into unrelated top-level features erases AnnotationSession as a unit from the directory structure altogether.

Recursive Child Features

What we adopted: place child features, used only within their parent, recursively inside each feature.

features/
  annotation-session/
    index.ts
    features/
      label-pane/
        index.ts
      task-navigation/
        index.ts

One giant feature no longer has to be carved up into unrelated top-level ones. AnnotationSession stays visible in the directory structure.

Each layer’s index.ts is its public surface. Direct imports of siblings’ or descendants’ internal files are not allowed.

Checking the Import Graph with ts-morph

Ideally, import constraints get enforced mechanically by a linter. But this convention depends on parent-child relationships.

The rule — “this feature may reference its child features’ index.ts, but not a grandchild’s internal files” — doesn’t fit the shape of an ordered layering. A simple ESLint layer rule couldn’t express it well.

So we built a graph of importer and resolved-importee paths with ts-morph and check it in CI. We look at which file at which layer imports from where, and reject any reference that doesn’t go through a public surface.

Writing conventions down in docs means humans have to catch violations during review. For rules about structure, checking them with a tool that can read structure is cheaper, which is why we settled on this approach.

When This Layout Pays Off

Recursively nested layouts aren’t a win at every scale.

It works when components and hooks used only within one capability grow alongside that capability. Conversely, turning a part shared across multiple screens into a child feature forces you to widen its public surface just to import it — defeating the whole point.

The criterion is whether the part makes sense detached from its parent grouping. If yes, it goes top-level; if not, it lives as a child feature.


Based on what I presented at our achievements presentation on July 31, 2026.