Engineering Journal
Schema Editor
Schema Editor

Split the Monolith Before You Need To, Not After

2026-06-04

TLDR

Common software engineering advice advocates starting with a single-file monolith and splitting code only when working in it becomes painful. The flaw in this advice is timing: "painful" occurs after months of feature additions, when shared scope has hidden dozens of implicit state dependencies, initialization races, and broken closure bindings. The ideal time to establish modular ES boundaries is when building your second major feature, establishing clean interface contracts while codebase complexity remains low.
Refactoring TimingCodebase ComplexityLatent Bug Surface AreaRefactoring Cost
Delayed (At 10th Feature)High (Entangled shared scope)Extensive (Dozens of hidden bugs)Extremely Expensive & Risky
Proactive (At 2nd Feature)Low (Clean modular boundaries)Minimal (Explicit contracts)Low (Compounds stability)

Problem statement: the deferred cost of late refactoring

The "start simple, refactor later" philosophy is popular because it defers architectural overhead during initial prototyping.

However, single-file scripts act as a forgiving environment that masks structural defects:

When teams wait until a file reaches 2,500 lines before attempting a modular split, removing the single-file environment exposes every hidden bug simultaneously, turning a routine refactor into weeks of regression debugging.


Technical failure mode: the refactoring regression avalanche

When refactoring a large legacy monolith into modules:

  1. Module Scope Breakage: Methods relying on implicit global variables throw reference errors.
  2. Initialization Race Conditions: Extracted modules attempt to execute code before dependent core engines initialize.
  3. CSS Load Order Inversions: Split CSS files break rule precedence that previously depended on inline script order.

The fix & architecture: proactive module isolation at feature 2

Establish ES module boundaries when adding your second major feature:

// Module 1: core/editor.js (Established in Feature 1)
export const editor = {
  getSelection() { / ... / }
};

// Module 2: features/layers.js (Established in Feature 2 via explicit imports) import { editor } from '../core/editor.js';

export function initLayerPanel() { const selection = editor.getSelection(); // Explicit contract, zero shared global state! }

Establishing module contracts early ensures every subsequent feature inherits clean encapsulation patterns automatically.

Rule of thumb: Do not wait for a single-file script to become unmaintainable before splitting it. Establish explicit ES module boundaries as soon as you build a second feature that shares application state.
Read this post in the full Engineering Journal →