def total = Billing::Proration
.for(self).adjust(subtotal) The problem
The common closure principle says that code that changes together should be kept together. A
directory layout departs from it through decisions that each made sense when they were taken. In the
example app Invoice went in app/models because it’s a model, and Proration went in
app/billing because it’s billing. The two call each other on every total.
app/models/invoice.rb:35:
def total
taxed = subtotal * (1 + rate_table.vat_for(region))
Billing::Proration.for(self).adjust(taxed)
end
app/billing/billing/proration.rb:5:
def self.for(invoice)
new(invoice.plan, invoice.period, invoice.lines)
end
A change to how proration reads an invoice means editing two directories and reviewing two areas of
the tree, and it depends on whoever owns app/models noticing. The layout does not show that these
two files work as one unit, and the source does not show how much they depend on each other compared
with everything else.
The fix
ra relocate_file reports which file a given file belongs with:
$ ra relocate_file app/billing/billing/proration.rb
app/billing/billing/proration.rb
· talks to 5 files
app/models/invoice.rb 520 370
app/models/money.rb 740 0
app/models/invoice_line.rb 147 0
app/billing/billing/checkout.rb 0 126
app/controllers/api/quotes_controller.rb 0 20
· app/billing/billing/proration.rb ⇄ app/models/invoice.rb
890 calls between them, in both directions
proration has 2 other callers, and invoice has 8 other callers
3 directories apart: app/billing/billing ⇄ app/models
→ gather both into app/models CCP
The two columns are the calls from proration.rb into each file and the calls back from it.
money.rb gets more traffic, but all of it goes one way, because proration uses Money the way the
rest of the app does. invoice.rb is the file that calls back, 370 calls to 520, so the tool pairs
it with proration.rb. The suggested folder is the one that already has most of the pair’s combined
traffic.
Move the file:
app/billing/billing/proration.rb → app/models/billing/proration.rb
With Zeitwerk the directory is the namespace, so Billing::Proration keeps its name. Its two other
callers are in the report, and what_a_change_touches lists every call site to update in the same
commit.
How it works
- The recording holds, for every call, the file its calling method was defined in and the file of the method it reached. Summed over every run, the calls form a weighted graph of files.
- For each pair it measures exclusivity: how much of each file’s traffic the other one accounts for, and how many other callers each side has. Two files that mostly call each other score high, and a utility that every file calls scores low.
- For distance, it counts the path segments that differ after the shared prefix.
app/billing/billingandapp/modelsshareappand differ in three segments. - It names the folder that puts the pair closest together while moving the least.
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 relocate_file app/billing/billing/proration.rb
The pair is app/models/invoice.rb:35 and app/billing/billing/proration.rb:5.
Reading the result
- If you group by type (
models,services,jobs) deliberately, it will keep suggesting that you group by domain, because common closure is the principle it measures. Read those suggestions as a description of the choice you have made. - It measures which files call each other. Which files change in the same commits is a different signal, available from git history, and you can check it alongside.
- It does not move files. Moving a file renames constants and breaks references the tool cannot see, such as strings in config, so the move is yours or your agent’s to make.
Every tool reads the same recording. What gets recorded and how far to trust an answer are on the questions page.
Related tools
break_cycle
Cut a dependency cycle. The files or namespaces that form a dependency cycle at runtime. Each cycle is marked with its lowest-traffic edge, which is the cheapest one to cut.
See more →split_package
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.
See more →what_a_change_touches
What a signature change affects. For each caller of a method, the argument classes it passed and the messages it sent to the return value, so you can see which callers a signature change affects before you make it.
See more →