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
- From every recorded call edge, it keeps the ones that enter the namespace from outside it. Calls
from one
Billingclass to another are left out. - 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.
- Consumers whose sets do not overlap go into separate groups, and each group’s classes are a
candidate package.
split_interfacedoes 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::Receiptthey 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.
Related tools
split_interface
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
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
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 →