runtimeanalyz.ing

← All capabilities

split_package

CLI · MCP Pro

The parts of a namespace each consumer uses. Groups the classes in a namespace by which outside classes called them, and reports the groups that share no consumer as the lines to split the namespace along.

Checkout
Gateway#charge ( amount Money )
Receipt.for ( charge Charge )
Billing Payments
Gateway · Receipt · Card
Refunds
Gateway#refund ( charge Charge )
Card#token
Reporting::Report
Proration.for ( invoice Invoice )
TaxTable#rate_for ( region Region )
Invoicing
Proration · TaxTable
Every call into Billing from outside it, grouped by consumer. Payments code and reporting code call two separate sets of methods, and no consumer calls both sets, so Billing is two packages under one name.

The problem

Putting classes in one namespace says they belong together. The common reuse principle tests that claim against the namespace’s consumers: a consumer that depends on one class in a package should need the rest of it too. A consumer that doesn’t is exposed to changes, reviews and releases for code it does not call.

Billing in the example app covers more than one concern:

app/billing/billing/
  checkout.rb            Billing::Checkout            the purchase
  gateway.rb             Billing::Gateway             charges and refunds
  receipt.rb             Billing::Receipt             one per charge
  card.rb                Billing::Card                tokenised cards
  proration.rb           Billing::Proration           partial periods
  rate_table.rb          Billing::RateTable           VAT by account
  rate_cache.rb          Billing::RateCache           a warm cache of rates
  tax_table.rb           Billing::TaxTable            rates by region
  tax_rules.rb           Billing::TaxRules
  currency_converter.rb  Billing::CurrencyConverter

A change to the rate cache goes into the payments team’s review queue. A packwerk package boundary around Billing would force every consumer through that one boundary, and extracting payments into a gem would take proration along with it. Where to split depends on which consumers call which classes, and the directory listing does not show that.

The fix

Ask how a namespace is consumed:

$ ra split_package Billing

Billing
· 3 consumer groups · 6 outside consumers · 0 consumers reach two groups
Enrollment   → Billing::RateCache
RenewalJob, Invoice, CheckoutsController, Api::QuotesController   → Billing::Checkout, Billing::Proration, Billing::RateTable, Billing::Receipt
Refund   → Billing::Gateway
· split it in 3 along these lines   CRP

There are three groups, and no consumer calls into two of them. Enrollment’s one use of Billing is warming the rate cache from an after_commit, and Refund’s is the gateway. The other four consumers, which buy or quote, use the checkout, the proration, the rate table and the receipt together. Split the namespace along the same lines:

app/billing/billing/rate_cache.rb  →  app/billing/billing/rates/rate_cache.rb     Billing::Rates::RateCache
app/billing/billing/gateway.rb     →  app/billing/billing/payments/gateway.rb     Billing::Payments::Gateway
app/billing/billing/checkout.rb    →  app/billing/billing/orders/checkout.rb      Billing::Orders::Checkout
…

After the split each consumer depends on the part it uses, and a change to the rate cache does not affect checkout. With packwerk, each part becomes its own package with its own public API.

How it works

  1. From every recorded call edge, it keeps the ones that enter the namespace from outside it. Calls from one Billing class to another are left out.
  2. For each outside class it collects the set of classes that class called inside the namespace. The result is a map from each consumer to the part of the package it depends on.
  3. Consumers whose sets do not overlap go into separate groups, and each group’s classes are a candidate package. split_interface does the same grouping one level down, on the methods of a class.

Run it on your app

The output on this page comes from our example Rails app, and it is regenerated whenever a tool changes. These are the commands behind it. On your own app, run ra record first, then put your own class, method or file in their place.

ra split_package Billing

The consumers are app/models/enrollment.rb:57, app/services/refund.rb:8 and the checkout callers in app/controllers and app/jobs.

Reading the result

  • Classes that two groups share are reported as overlap. If two groups both used Billing::Receipt they would not be disjoint, and the report would list the shared classes as a common core instead of assigning them to one group.
  • The tool finds the groups. What to call them and where the files go are decisions about your domain, and it leaves both to you.

Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.

split_interface

CLI · LSP · MCP Pro

The role interfaces in a class. Groups a class's public methods by which callers used them in the recording, and lists each group as a role interface to extract.

See more →

stabilize_dep

CLI · MCP Pro

Dependencies that run against stability. A stable namespace, one with many callers and few dependencies, calling into a less stable one. Instability is computed from recorded calls, and the report lists the call sites to move or to put behind an abstraction.

See more →

relocate_file

CLI · MCP Pro

Which file should move where. Two files that call each other often and are directories apart. The report names the folder they belong in, weighted by recorded call traffic and the distance between them.

See more →