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.
| Axis | Strongest when... | Main risk |
|---|---|---|
| Feature | change and ownership are the main concern | duplication if not governed |
| Layer | domain boundaries are already stable | cross-feature coupling and "shared" sprawl |
| Technical concern | low-level platform or infrastructure work | code 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 category | Purpose |
|---|---|
| transport types | reflect external contract shape |
| domain types | reflect business meaning |
| UI types | reflect 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
- TypeScript handbook - https://www.typescriptlang.org/docs/