runtimeanalyz.ing

← All capabilities

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.

Billing
debit
4.1M calls
Finance::Accounts
unlock_paid_books
3.9M calls · not on the wiki
Curriculum
charge
4.1M calls
Gateway
after_commit callback
1.2M calls · cycle
Enrollment
Reporting
monthly_totals
96 calls · month-end
Billing
Edges between namespaces, weighted by the calls recorded in app runs. An extraction starts with the heavy edge and with the cycle that runs through a callback. The month-end edge can be left for later.

The problem

Suppose you want to pull billing out of the monolith, and the diagram on the wiki shows Billing calling Finance alone. The code has other dependencies, and finding them means reading all of it. This is app/billing/billing/checkout.rb:8 in the example app:

def complete(order)
  invoice = order.invoice || Invoice.new(account: order.account, number: order.number, issued_on: Date.current)
  card = Card.for(order.account)
  receipt = @gateway.charge(order.total)
  Finance::Accounts.new.debit(order.account, order.total, memo: "#{order.number} #{card.masked}")
  invoice.save! if invoice.new_record?
  Proration.for(invoice).amount
  @notifier.deliver(invoice)
  Curriculum.for(order.student).unlock_paid_books(order.student)
  receipt
end

The method calls into Curriculum. A static dependency graph would find that call, but it would not say whether Billing → curriculum runs once a month or on every checkout. A static graph also misses the calls made through send, callbacks and ActiveSupport::Notifications.

The fix

ra coupling_map counts the calls in the recorded app runs. Spec trees are left out, so these are the calls that requests and jobs made:

$ ra coupling_map --namespace Billing
coupling_map · Billing
· pooled from 1 recorded run · 150 call trees (a namespace is a class's leading module, or for a top-level class the file's first path segment after app/<layer>/)
  Billing               →  money           740
  invoice               →  Billing         661  ⚠ cycle
  Billing               →  invoice         478  ⚠ cycle
  Billing               →  order           224
  checkouts_controller  →  Billing         200
  Billing               →  account         156
  Billing               →  plan            156
  Billing               →  invoice_line    136
  Billing               →  Finance         116
  Billing               →  curriculum      116
  Billing               →  email_notifier   50  ⚠ cycle
  renewal_job           →  Billing          24
  Api                   →  Billing          20
  Billing               →  null_notifier     8
  refund                →  Billing           5

The extraction plan can start from these recorded edges. Billing → curriculum ran 116 times, once per checkout, so the plan replaces the direct call with an event. Start with the Billing ⇄ invoice cycle: Invoice#total asks Billing::Proration for its adjustment, and Proration.for reads the invoice back.

def complete(order)
  # …
  @notifier.deliver(invoice)
  Billing.events.publish(:paid, order:)
  receipt
end

How it works

Every recorded call has a caller and a receiver, and each of them is defined in a file and belongs to a namespace. The tool groups the calls in the app-run trees from methods up to classes and then to namespaces, counting them at each level, and marks an edge as a cycle when the reverse edge also exists. A call site whose source line contains after_commit, send, public_send or ActiveSupport::Notifications is marked with how the call was made.

break_cycle and stabilize_dep read the same recording and include spec trees. coupling_map does the same counting over the app-run trees alone.

The hosted graph is built and is not open yet. It will pool runs from every deploy, so that the summary line counts deploys and days where it now counts one recorded run. Rendering the map in the browser is planned.

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 coupling_map --namespace Billing

The call into Curriculum is app/billing/billing/checkout.rb:16.

Reading the result

  • The counts measure traffic. An edge that is rarely called can still be critical: refund → Billing ran five times, and the business cares about every one of those calls.
  • Rows are grouped by namespace. A top-level class is its own namespace, named after its file, so invoice and money appear alongside Billing and Finance. Nest your classes in modules, or read those rows as classes.

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 →

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 →

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 →