Engineering Journal
Pdf Processor
Pdf Processor

A 1000-Line File Is Not a Code Problem. It Is a Design Problem.

2026-05-31

TLDR

Refactoring a 1,000-line file by creating a utils.js helper file or wrapping procedural code in a stateful class fails to solve the root problem: implicit coupling across sequential execution passes sharing mutable scope. Document layout classification is a data-flow pipeline. Decoupling the monolith into independent single-function domain modules called by a linear orchestrator converts hidden ordering dependencies into explicit, type-checked function arguments.
Refactoring StrategyCode OrganizationScope IsolationOrdering Dependency Visibility
Extract utils.js HelpersShorter main file + Utility dumping groundLow (Shares mutable closure state)Hidden (Implicit top-to-bottom execution)
Wrap in Stateful ClassMethods bound to this instanceLow (Instance properties read/written everywhere)Hidden (Method invocation order dependency)
Decoupled Pipeline ModulesPure single-function domain modulesHigh (Explicit arguments & return values)100% Explicit (Visible call sequence)

Problem statement: the myth of the "helper extraction" refactor

When procedural files grow past 1,000 lines, developers frequently apply standard refactorings:

  1. Extract utils.js: Move math calculations or regex helpers out of the file. The file shrinks to 900 lines, but the sequential procedural coupling remains completely unchanged.
  2. Wrap in Class: Create a class DocumentClassifier that stores state on this. Methods read and write this.regions at arbitrary execution steps, concealing ordering constraints behind object instance state.
Neither approach addresses the underlying flaw: sequential layout classification passes sharing mutable state without explicit interface contracts.


Technical architecture: pipelines vs. Stateful objects

Document layout classification is not a stateful object. It is a linear data processing pipeline:

[Raw Items] ---> [Columns] ---> [Tables/Images] ---> [Text Blocks] ---> [Dividers] ---> [Sorted Output]

The decoupled module architecture

Each classification domain resides in its own module exporting a single pure function with explicit parameters:
// dividerDetector.js: Takes explicit inputs, returns explicit array
export function detectDividers(lineSegments, existingRegions, viewport) {
  return lineSegments.hLines.filter(seg => {
    return !existingRegions.some(r => isBBoxContained(r.bbox, seg));
  });
}

The function signature explicitly communicates its ordering contract: detectDividers requires existingRegions to be populated with text blocks before it can run.

The pipeline orchestrator defines the sequence:

// orchestrator.js: Sequence is documented directly in function call order
const textBlocks = classifyTextBlocks(textItems, columns);
regions.push(...textBlocks);

// Explicit dependency: detectDividers receives populated textBlocks const dividers = detectDividers(lineSegments, regions, viewport); regions.push(...dividers);

Rule of thumb: Treat multi-pass document classifiers as linear data pipelines with explicit function arguments rather than stateful classes or procedural monoliths.
Read this post in the full Engineering Journal →