Skip to main content

Codebase & Module Architecture

(How to structure frontend systems that scale in time and teams)

Architect rule:

If the codebase cannot explain its own boundaries, the architecture only exists in people's heads.

Why Codebases Decay

1.1 The Entropy Curve

Frontend codebases rarely collapse because one developer made a bad choice. They collapse because boundaries remain implicit while change keeps increasing.

Common symptoms:

  • "Where should this code live?"
  • "Put it in shared for now."
  • "We will clean it up later."
  • fear of touching an existing module

These are structural signals, not personality flaws.

1.2 The Root Cause: Missing Boundaries

Without boundaries:

  • coupling spreads invisibly
  • refactors become risky
  • ownership becomes unclear
  • onboarding gets slower every month

Architecture Is More Than Folder Structure

2.1 Visibility Matters

Folder structure alone is not architecture, but invisible architecture drifts quickly. Strong systems encode boundaries in:

  • module layout
  • public APIs
  • dependency rules
  • tooling
  • ownership

2.2 Valid Axes of Organization

You can organize by more than one dimension, but you usually need one primary axis.

AxisStrongest when...Main risk
Featurechange and ownership are the main concernduplication if not governed
Layerdomain boundaries are already stablecross-feature coupling and "shared" sprawl
Technical concernlow-level platform or infrastructure workcode placement becomes hard to reason about

For most growing products, feature is the best primary axis and other axes remain local within a feature or package.

Feature-Based Structure

3.1 Why It Usually Wins

Feature-based architecture tends to optimize for:

  • ownership
  • locality of change
  • safer refactoring
  • more obvious responsibility boundaries

3.2 Reference Structure

This is a reference model, not a law. The important point is that the structure exposes dependency direction and ownership.

3.3 High Cohesion, Low Coupling

Inside a feature, moderate local complexity is acceptable. Between features, coupling should be narrow, visible, and preferably mediated through public APIs.

Dependency Direction

4.1 A Useful Default

A practical default is:

That does not make every architecture valid or invalid by itself. It is a useful baseline because it limits sideways dependency drift.

4.2 Public APIs vs Private Internals

Treat each module like a small package:

  • define a stable public surface
  • hide internals
  • document what consumers are allowed to rely on

This reduces accidental coupling and makes refactors survivable.

4.3 Boundary Review Checklist

Ask these questions during review:

  • Can one feature import another feature's internals?
  • Is domain logic hiding in shared utilities?
  • Does a package expose more than consumers truly need?
  • Can a new engineer tell where new code belongs?
  • Would a module move require touching unrelated features?

TypeScript as an Architectural Tool

5.1 Types Express Boundaries

Types are not only safety nets. They can communicate:

  • domain meaning
  • public contracts
  • conversion boundaries
  • permitted extension points

5.2 Domain Types vs UI Types

Do not let raw backend payloads become permanent UI contracts by accident. Translate where needed.

Useful separation:

Type categoryPurpose
transport typesreflect external contract shape
domain typesreflect business meaning
UI typesreflect presentation and interaction needs

5.3 When Branded or Opaque Types Help

Use branded or opaque types when the cost of mixing values incorrectly is high enough to justify the extra ceremony.

The Shared Utilities Trap

6.1 Why Shared Turns Into a Graveyard

Shared becomes dangerous when it stores:

  • domain logic with no owner
  • convenience helpers that encode feature assumptions
  • unstable abstractions promoted too early

6.2 Rules for Shared Code

Good shared code is usually:

  • low-level
  • broadly reusable
  • well-owned
  • dependency-light

If code is only shared by two neighboring features, it often belongs closer to those features.

Refactoring and Migration

7.1 Refactoring Is Architectural Work

Architectural refactoring is not cleanup theater. It is how teams recover safer boundaries over time.

7.2 Safe Refactoring Principles

  • add a new path before removing the old one
  • expose migration-friendly public APIs
  • keep telemetry during transition
  • remove dead paths deliberately, not accidentally

7.3 Strangler Patterns in Frontend

Frontend strangler patterns work well when:

  • an old module can be wrapped
  • traffic can move surface by surface
  • consumers can migrate incrementally

Testing and Tooling for Boundaries

8.1 Tooling as Policy

Use tooling to enforce architecture where practical:

  • linting
  • import constraints
  • package boundaries
  • CI policy for public API changes

8.2 Test the Contract, Not Just the Implementation

Tests should verify:

  • public module contracts
  • important inter-module interactions
  • migration safety for shared packages

Behavioral tests usually add more long-term value than tests that only mirror file structure.

Review Checklist

  • Is the primary organization axis obvious?
  • Are public APIs explicit?
  • Are dependency directions enforceable?
  • Is "shared" small, owned, and boring?
  • Can modules evolve without widespread breakage?

Exercises

Exercise 1 - Boundary Mapping

Map your codebase into:

  • features
  • entities
  • shared or platform layers

Mark every sideways dependency.

Exercise 2 - Public API Audit

Pick three modules and identify:

  • what is public
  • what should be private
  • which current consumers rely on internals

Exercise 3 - Shared Code Cleanup

Take one shared folder and label every item:

  • truly shared
  • should move to a feature
  • should be deleted

Further Reading