runtimeanalyz.ing

← All capabilities

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.

Against stability

Finance::Accounts instability 0.12
Called from 14 namespaces. Calls into 2.
to_ledger_s
8,930 calls from 1 call site
Formatters::Currency instability 0.81
Called from 2 namespaces. Calls into 9.

Toward stability

Formatters::Currency instability 0.81
to_ledger_s
the churn depends on the core
Finance::Money instability 0.12
Stability is counted from recorded callers: a namespace with many callers and few dependencies is stable. The ledger called into a report formatter, and moving one method reverses that dependency.

The problem

Some code has many dependents. In the example app Finance is called from five namespaces and calls into three, so a change to it has to be made slowly and carefully. Other code is expected to change often: Reporting is called from three namespaces, and it changes every time someone wants a new column.

The stable dependencies principle says dependencies should point from code that changes often to code that changes rarely. One dependency in the example app points the other way, in app/finance/finance/accounts.rb:14:

def debit(account, amount, memo:)
  raise InsufficientFunds, "#{account.balance} < #{amount}" if account.balance < amount

  account.balance = account.balance - amount
  account.save!
  entry = @ledger.record(account, Money.zero - amount, memo: memo)
  @audit.log(Reporting::Formatters::Currency.new(amount).to_ledger_s)
  entry
end

With that line, a change to a report formatter can break every debit in the app. The dependency came from convenience: someone needed a currency string and the formatter was already written. Reading the file does not show the problem, because how stable each side is comes from counting who calls whom.

The fix

Ask which dependencies run against stability:

$ ra stabilize_dep finance

Finance
· instability 0.38   called from 5 namespaces, calls into 3 (a namespace is a class's leading module, or for a top-level class the file's first path segment after app/<layer>/)
  account    0.29  309  fine
  money      0.00  489  fine
  Reporting  0.40  178  against stability
⚠ Finance → Reporting   against stability
  Finance      instability 0.38   called from 5 namespaces, calls into 3
  Reporting    instability 0.40   called from 3 namespaces, calls into 2
  1 call site, 178 calls:
    Finance::Accounts#debit → Reporting::Formatters::Currency#initialize
    Finance::Accounts#debit → Reporting::Formatters::Currency#to_ledger_s
  → move it into Finance, or depend on an abstraction Finance owns   SDP · DIP

The whole dependency is one call site, so the cheap fix is to get the string from Money, which already has Money#to_ledger_s in app/models/money.rb:43:

def debit(account, amount, memo:)
  raise InsufficientFunds, "#{account.balance} < #{amount}" if account.balance < amount

  account.balance = account.balance - amount
  account.save!
  entry = @ledger.record(account, Money.zero - amount, memo: memo)
  @audit.log(amount.to_ledger_s)
  entry
end

Reporting::Formatters::Currency can call Money#to_ledger_s if it wants the same string, and then the dependency points toward the stable side. When a dependency is too big to move, the other option is dependency inversion: Finance defines the role it needs, and Reporting supplies an object that plays it.

How it works

  1. From every recorded call edge, rolled up to namespaces, it counts the distinct namespaces that call into each one and the distinct namespaces it calls out to.
  2. Instability is calls out divided by calls in plus calls out, which gives a number from 0 (everyone depends on it and it depends on nothing) to 1 (nothing depends on it and it depends on everything). money scores 0.00 because everything calls it and it calls nothing.
  3. Any edge from a more stable namespace to a less stable one is reported, with its traffic and the call sites that make it. The edges come from recorded calls, so a dependency made through send or a callback counts the same as a direct one.

In an app this small the gap is narrow, 0.38 against 0.40. The report prints both numbers and the caller counts behind them, so you can judge whether the gap means anything.

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 stabilize_dep finance

The call is at app/finance/finance/accounts.rb:20.

Reading the result

  • Stability is measured from what ran. A namespace your recorded runs barely touched has too few callers to measure fairly, and the report says how many calls each number is based on.
  • There is no abstractness score. The original metric also weighs how abstract a package is, and Ruby has no interfaces to count, so the report stops at instability and leaves the abstraction to you.
  • Some flagged dependencies are deliberate, such as a stable core calling out to a plugin point. The report is a hint and does not fail a build.

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

break_cycle

CLI · MCP Pro

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

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.

See more →

coupling_map

CLI · MCP Pro

Coupling measured from app runs. Which modules and namespaces of your app call each other, and how many times, counted from recorded app runs.

See more →