Engineering Journal
Schema Editor
Schema Editor

Split the Monolith Before You Need To, Not After

2026-06-04

TLDR

The common advice is to start with a monolith and split when it gets painful. The problem: "painful" is when the split is hardest. Every latent bug that the shared closure hid becomes visible during the split. Split at the second major feature, when the pattern is still simple enough to establish cleanly.


What the Industry Does

The "start simple, refactor later" pattern is universal advice. Write a single file. Get the feature working. When the file gets too large to navigate, split it. This is correct about starting simple. It is wrong about when to split.

The advice persists because the cost of the split is deferred. The monolith grows. Working in it gets slower. Eventually the pain exceeds the cost of the split, and the split happens. This is rational. It is also the worst possible time.

Why It Fails for This Problem Class

A monolith's shared closure scope hides bugs. Arrow functions using $(this) work because the outer this has the right data. Initialization order errors do not surface because everything runs in sequence. Implicit state dependencies are invisible because everything shares the same scope.

None of these bugs exist because of the monolith. They exist because the code is wrong in ways that the monolith's environment compensates for. The split removes the compensation. Every hidden bug becomes visible simultaneously.

When the monolith is 2500 lines and has been in production for months, "every hidden bug becomes visible simultaneously" means weeks of debugging during the split. Features break that appeared to work. Users report regressions. The split is blamed, but the split did not cause the bugs.

The Better Approach

Establish module boundaries at the second major feature. The first feature defines one module. The second feature defines a second module with an explicit interface to the first. This is the moment when the pattern is still simple: two files, one interface, no legacy debt.

The third feature follows the established pattern. The fourth follows it. By the time the codebase is large, the module boundaries are already correct and the split has already happened.

The cost is higher abstraction earlier than it feels necessary. Writing an explicit export for a function that only one other module calls feels over-engineered when the codebase is small. It is not over-engineered. It is the correct time to establish the constraint.

What You Give Up

Monolith development is faster for the first feature. Everything in one place, no imports, no module boundaries to reason about. If the project will only ever have one major feature, staying in one file is the right choice. The cost of the modular pattern is only worth paying when the codebase will grow to where the monolith becomes a liability.

The practical test: does this codebase have two major features that share state? If yes, the module boundary should exist now, not later.

When the Common Pattern Is Right

Start with a monolith and split later when: the project is exploratory (you do not know yet what the features are), the codebase will be thrown away if the prototype fails, or the features are genuinely independent with no shared state. A monolith with no shared state splits cleanly at any time because there is nothing to untangle.

Read this post in the full Engineering Journal →