Codebase & Module Architecture
(How to structure frontend systems that scale in time and teams)
Architect rule:
If your codebase structure cannot explain itself,
your architecture exists only in your head.
Chapter 1 — Why Most Frontend Codebases Collapse
1.1 The Entropy Curve
Frontend codebases don’t fail because of:
-
bad developers
-
wrong frameworks
-
lack of tests
They fail because of structural entropy.
Symptoms:
-
“Where should this code live?”
-
“Just put it in shared”
-
“We’ll refactor later”
-
Fear of touching existing code
These are architectural failures, not human ones.
1.2 The Root Cause: Missing Boundaries
Without boundaries:
-
everything depends on everything
-
refactors ripple unpredictably
-
ownership dissolves
-
velocity drops as team size grows
Architect principle:
Boundaries are more important than abstractions.
Chapter 2 — Architecture ≠ Folder Structure (But It Must Be Visible)
2.1 The Architecture Visibility Problem
Architecture that is:
-
only documented
-
only discussed
-
only “understood”
…will drift.
Architects encode architecture:
-
in folder structure
-
in dependency rules
-
in TypeScript boundaries
-
in tooling constraints
If architecture is not enforced, it is optional.
2.2 The Three Valid Axes of Code Organization
Frontend code can be organized by:
-
Layers (UI, domain, data)
-
Features (auth, billing, dashboard)
-
Technical concerns (hooks, utils, services)
Architects choose one primary axis
and allow others only locally.
Chapter 3 — Feature-Based Architecture (Architect Default)
3.1 Why Features Beat Layers at Scale
Layered architecture looks clean early:
/components
/hooks
/services
/utils
But it optimizes for code reuse, not change.
Feature-based architecture optimizes for:
-
ownership
-
locality
-
safe refactoring
3.2 Feature-Based Structure (Reference Model)
/app
/features
/auth
auth.routes.ts
auth.api.ts
auth.model.ts
auth.ui.tsx
/billing
/dashboard
/entities
/user
/order
/shared
/ui
/lib
/config
Each feature is:
-
cohesive
-
semi-independent
-
refactorable in isolation
3.3 Architect Rule: High Cohesion, Low Coupling
Inside a feature:
- tight coupling is acceptable
Between features:
- coupling must be explicit and minimal
This is how architects allow local complexity
without creating global chaos.
Chapter 4 — Dependency Direction (The Hidden Backbone)
4.1 The One Rule That Saves Codebases
Dependencies must point inward, never sideways.
This idea originates from classical architecture thinking (popularized by Clean Architecture), but frontend teams rarely enforce it.
4.2 Allowed Dependency Flow
app → features → entities →shared
Rules:
-
featuresmay depend onentities -
entitiesmust never depend onfeatures -
shareddepends on nothing
If this is violated, refactors become landmines.
4.3 Sideways Dependencies Are Poison
Anti-pattern:
-
authimporting frombilling -
dashboardreaching intoorders/internal
Architect solution:
-
move shared logic to
entitiesorshared -
expose public APIs, hide internals
Chapter 5 — Public APIs vs Private Internals (Critical)
5.1 Modules Are Mini-Packages
Architects treat each module like an npm package:
-
public surface
-
private internals
-
stable contracts
Example:
/auth
index.ts ←public API
auth.api.ts
auth.internal.ts
Only index.ts is importable.
5.2 Why This Matters
Without public APIs:
-
consumers depend on internals
-
refactors break silently
-
ownership dissolves
Architect rule:
If it can be imported, it will be depended on.
Chapter 6 — TypeScript as an Architectural Tool
6.1 Types Are Not Just Safety Nets
Frontend architects use TypeScript to:
-
encode boundaries
-
prevent illegal access
-
document contracts
Types are architecture, not syntax.
6.2 Domain Types vs UI Types
Architect separation:
-
Domain types → business meaning
-
UI types → rendering concerns
Example:
typeUser = {
id:UserId
role:UserRole
}
typeUserCardProps = {
user:User
isSelected:boolean
}
UI never mutates domain meaning.
6.3 Branded & Opaque Types
Architects prevent misuse via types:
typeUserId =string & {__brand:'UserId' }
This:
-
prevents mixing IDs
-
catches bugs early
-
documents intent
Chapter 7 — The “Shared Utils” Trap
7.1 Why Shared Becomes a Graveyard
/shared often contains:
-
unrelated helpers
-
feature-specific hacks
-
abandoned code
This happens when:
-
ownership is unclear
-
boundaries are missing
-
reuse is premature
7.2 Architect Rules for Shared Code
Only move code to shared if:
-
used by multiple features
-
conceptually generic
-
ownership is clear
-
API is stable
Otherwise:
- duplication is cheaper
Duplication is often less costly than wrong abstraction.
Chapter 8 — Refactoring Without Fear (Architect Strategy)
8.1 Refactoring Is Architectural Work
Refactoring is not:
-
cleanup
-
polishing
-
tech debt payment
It is re-aligning code with architecture.
8.2 Safe Refactoring Principles
Architects ensure:
-
boundaries are enforced
-
APIs are explicit
-
tests cover behavior, not structure
This allows:
-
large changes
-
incremental migrations
-
confidence
8.3 Strangler Patterns in Frontend
Architects migrate by:
-
introducing new boundaries
-
routing traffic gradually
-
isolating legacy code
Not by rewriting everything.
Chapter 9 — Tooling to Enforce Architecture
9.1 Linting as Policy
Architects use lint rules to:
-
enforce import boundaries
-
prevent forbidden dependencies
-
encode architectural rules
If violations compile, architecture is optional.
9.2 Monorepo Constraints
In monorepos:
-
enforce dependency graphs
-
restrict cross-package imports
-
define ownership clearly
This scales architecture beyond one team.
Chapter 10 — Exercises (Mandatory)
Exercise 1 — Boundary Mapping
Draw your current codebase.
Identify:
-
implicit boundaries
-
violated dependency directions
Exercise 2 — Public API Audit
Pick one feature.
List:
-
what should be public
-
what must be private
Create an index.ts accordingly.
Exercise 3 — Shared Code Cleanup
Review /shared.
For each item:
-
justify existence
-
assign ownership
-
delete or relocate if unclear
Chapter 11 — Part IV Summary
After Part IV, you should:
-
Design codebases for change, not elegance
-
Use features as the primary unit of architecture
-
Enforce dependency direction
-
Treat modules like packages
-
Use TypeScript as an architectural guardrail
If Part III answered “How data moves”