Modularization

Split large monoliths into modules.

A split starts with measurement. The engine resolves the whole codebase and computes the structure it actually has: which parts depend on each other, how the code clusters, where the loops are and what holds them together. Where a boundary can go, and what moving it would cost, comes out of those numbers. It can be done in one pass or one boundary at a time.

Where the line can go

Every candidate is priced before anything moves.

Cycles and components, how tightly each part is coupled, how the code clusters against how it is filed, which edges invert an intended layering. All of it computed on the resolved codebase rather than read off the packages.

Candidates

The code already groups by who calls whom. Set that against how it is filed into packages. The natural boundaries show up, and those are the lines worth pricing.

Cycles

A loop has no first element, so no line passes through one. We find the references that hold a loop together and the smallest set of moves that breaks it.

Price

For each candidate: what still crosses the line, in which direction, what has to move, and what the build files would need. Candidates get compared by number.

What we do to the code

The kinds of operation a split uses.

An operation is one instruction the engine carries out everywhere it applies. Which of these a split needs depends on the codebase.

Move a set of types

Into a build unit of their own, with every reference, import, module declaration and build file rewritten to match.

Extract an interface

Derived from what callers actually use, so the members are decided by demand rather than named by hand.

Split a class or a method

A type that is two utilities in one name comes apart, and what belongs to each side goes with it.

Retype declarations

Point a field, parameter or local at the most general type that still compiles, across every use at once.

Replace construction by name

A dependency that exists only because something is constructed by name becomes one the type graph can see.

Confine an external library

The code comes to depend on a generated contract, and only that contract depends on the library. It stops spreading.

Remove what nothing needs

Unreachable code, unused imports, access wider than anything uses. Taken out first, because it shrinks what has to move at all.

Write the build and the descriptors

Module declarations, service registrations and build files, kept in step with the code by the same operation.

The sequence

Splitting as a continuous process.

Measure what is knotted together, reduce it until a boundary can exist, price the candidates, apply the move, verify against the full test suite. Then again, for the next boundary.

Every step can be run as a question first. Reads answer without writing anything, and an operation can return exactly what it would change without changing it.

Scale

One instruction, every edit it implies.

An operation is one decision. The engine works out everything that has to change with it and applies it in a single action. A sense of the magnitudes involved, from operations run on real codebases:

OperationEdits
Move a tightly coupled set of 27 types
5,600
Move 350 types into a module
5,500
Move 100 types into a module
760
Relocate one public class
350
Relocate one class
110

The edit count does not follow the size of the move. A tightly coupled set of 27 types can cost more than a loose set of 350, which is why a boundary is priced rather than estimated.