When to Split a Monolith: It Is Not About Line Count
TLDR
Deciding when to refactor a single-file codebase into ES modules based on arbitrary line count thresholds (e.g., "split at 1,000 lines") is the wrong signal. A 2,500-line single file with clean, linear execution can be fine, while a 500-line file with shared closures and ambiguous state mutation can be unmaintainable. The correct trigger for splitting a monolith is ambiguous state ownership: when developers can no longer identify which module initializes, validates, and owns a piece of state.| Refactoring Signal | Arbitrary Line Count Triggers | State Ownership Triggers |
|---|---|---|
| Primary Metric | File line count ($> 1000$ lines) | Ambiguous state mutation / ownership |
| Splitting Strategy | Artificial splits (CSS vs JS vs UI) | Domain-driven ownership boundaries |
| Architectural Outcome | Circular imports & broken closures | Clean encapsulation & explicit interfaces |
Problem statement: the flaws of line-count refactoring
As frontend codebases grow, single-file scripts accumulate canvas drawing logic, spatial indexing, history stacks, layer UI, and export routines.
The conventional instinct is to split the file when it hits an arbitrary size limit.
However, splitting code based purely on file size often creates artificial boundaries, placing shared state variables in one file while leaving dependent functions in another, introducing circular imports and broken closures.
Technical failure mode: hidden bugs in shared closures
Inside a single-file script, shared scope masks three classes of architectural defects:
- Implicit Closure State: Functions mutate global variables without explicit parameters.
- Coincidental
thisBindings: Arrow functions using$(this)inside event handlers work by coincidence becausethispoints to a global module scope containing matching attributes. - Execution Order Dependencies: Top-to-bottom script execution masks missing initialization sequences.
The fix & architecture: domain-driven ownership boundaries
Split codebases based on clear state ownership signals:
- Signal 1 (Ambiguous Ownership): If multiple features mutate a shared variable (e.g.,
selectedElements), refactor the variable into a dedicated owner module. - Signal 2 (Cross-Domain Imports): If modifying wire routing requires inspecting layer panel code to trace shared variables, define an explicit interface contract.
// REFACTORED: Domain-Driven Module Structure
// 1. core/svgEditor.js -> Owns SVG DOM root, canvas camera, selection state export const editorCore = { getSelection() { / ... / }, setSelection(ids) { / ... / } };
// 2. canvas/canvasEngine.js -> Owns spatial indexing & direct drag interactions export const canvasEngine = { drawWire(from, to) { / ... / } };
// 3. features/layers.js -> Owns layer panel UI generation export const layerPanel = { rebuild() { / ... / } };
Rule of thumb: Never split code based on line counts. Split when state ownership becomes ambiguous, making each module responsible for initializing and validating its own state.