Engineering Journal
Schema Editor
Schema Editor

When to Split a Monolith: It Is Not About Line Count

2026-06-04

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 SignalArbitrary Line Count TriggersState Ownership Triggers
Primary MetricFile line count ($> 1000$ lines)Ambiguous state mutation / ownership
Splitting StrategyArtificial splits (CSS vs JS vs UI)Domain-driven ownership boundaries
Architectural OutcomeCircular imports & broken closuresClean 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:

  1. Implicit Closure State: Functions mutate global variables without explicit parameters.
  2. Coincidental this Bindings: Arrow functions using $(this) inside event handlers work by coincidence because this points to a global module scope containing matching attributes.
  3. Execution Order Dependencies: Top-to-bottom script execution masks missing initialization sequences.
Splitting the file without resolving state ownership exposes all latent bugs simultaneously.


The fix & architecture: domain-driven ownership boundaries

Split codebases based on clear state ownership signals:

  1. Signal 1 (Ambiguous Ownership): If multiple features mutate a shared variable (e.g., selectedElements), refactor the variable into a dedicated owner module.
  2. 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.
Read this post in the full Engineering Journal →