John Ousterhout and Robert C. Martin agree on the objective, code that another programmer can understand and change, but disagree on the mechanisms. The dispute centers on method size, comments, and test-driven development.1
The Ruby verdictRuby should borrow Martin’s pressure toward readable names and fast feedback, but use Ousterhout’s “deep module” and cognitive-locality tests as the stopping rule. A line-count limit is a prompt for review, not a decomposition algorithm.
The Ruby verdict in detail
Methods
Prefer cohesion over raw brevity
A 15-line Ruby method that keeps one invariant visible can be easier to understand than five three-line private helpers that mutate shared state.
Abstraction
Extract deep boundaries
Extract when the new method or object hides meaningful detail behind a stable contract, not merely because a linter counted ten lines.
Comments
Document contracts and reasons
Let code explain mechanics. Use prose for intent, side effects, units, ordering constraints, external quirks, and public API behavior.
Testing
Use TDD as a mode, not a creed
Tiny red-green-refactor cycles are strong for defects and well-understood behavior. For a new domain boundary, sketch the design and acceptance examples first, then work in small tested bundles.
The compact Ruby rule
Where the two approaches differ
The repository records discussions held between September 2024 and February 2025. Its latest visible commit is dated 15 April 2025.12 The authors substantially agree that modularity, readability, unit testing, and professional judgment matter. Their disagreement is about trade-offs and failure modes.
Method length
| Martin / Clean Code emphasis | Ousterhout / APoSD emphasis | Ruby synthesis |
|---|---|---|
| Very small, well-named methods expose intent and separate concerns. | Over-decomposition creates shallow interfaces and forces readers to reconstruct a call graph. | Short is a useful smell detector; “deep enough to hide something” is the extraction criterion. |
| “One thing” plus judgment guides decomposition. | “One thing” lacks a guardrail because nearly any fragment can be named. | Ask whether the caller can safely forget the implementation. If not, the helper may be ceremonial. |
| Some entanglement is acceptable when names expose the high-level algorithm. | Entangled helpers defeat locality because understanding one requires loading the others. | Private helpers sharing mutable instance state deserve particular suspicion in Ruby. |
Ruby’s mainstream tooling contains a strong short-method bias: RuboCop enables
Metrics/MethodLength by default with a default maximum of 10 lines, while also making
the threshold configurable and excluding comments by default.3
The community Ruby Style Guide similarly advises avoiding methods over 10 LOC and says most should
be under five.4 These are conventions, not evidence that every
eleventh line should become a new method.
Comments
| Martin / Clean Code emphasis | Ousterhout / APoSD emphasis | Ruby synthesis |
|---|---|---|
| Prefer expressive code and names; comments are frequently stale, redundant, or misleading. | Important design information cannot always be encoded in executable syntax. | Delete narration; preserve contracts, rationale, invariants, and surprises. |
| Public APIs need more documentation than intimate team internals. | Internal interfaces also need documentation because memory and team membership decay. | Documentation depth should track boundary distance, risk, and hidden behavior, not visibility alone. |
| Long names are harder to ignore than comments. | Long names cannot encode every precondition, side effect, or reason. | Use idiomatic names for “what”; use comments/RDoc/RBS for the rest of the contract. |
Ruby guidance itself is split. The community guide opens its comment section with a deliberately anti-comment stance, but then explicitly recommends rationale comments.4 Shopify’s Ruby guide says to include missing context, keep comments synchronized, avoid superfluous narration, and focus on why.5 Ruby’s own documentation guide says too little information is bad and that documentation should quickly convey usefulness and usage.6
Test-driven development
| Martin / TDD emphasis | Ousterhout / “bundling” emphasis | Ruby synthesis |
|---|---|---|
| Very short test/code cycles maintain coverage, reduce debugging distance, and support refactoring. | Microscopic cycles can make the next test, not the system design, the center of attention. | Choose cycle size according to uncertainty: behavior uncertainty favors examples; architecture uncertainty favors a short design spike. |
| Design still occurs before and during TDD. | Larger implementation bundles create room to think about related abstractions together. | Write acceptance-level examples early, then alternate design reflection with small implementation steps. |
| Tests are executable low-level documentation. | Tests do not replace a coherent abstraction or prose explaining intent. | RSpec examples document behavior; they should not mirror every private method. |
RSpec’s own introduction strongly reflects Martin’s side: add a failing example, implement the behavior, take very small steps, and refactor before the code becomes messy.7 Rails, however, supports multiple test levels, model, controller, integration, and system tests, so a Ruby team is not limited to microscopic unit-first development.8
What Ruby changes
Line counts are unusually noisy
Blocks, implicit returns, modifier conditionals, keyword arguments, and endless methods compress behavior. Ruby has supported one-expression shorthand method definitions since Ruby 3.0.9 Ten Ruby lines can carry substantially different cognitive weight depending on DSL and control flow.
Extraction is nearly frictionless
Because helpers are cheap to create, Ruby code can drift into a stack of tiny private methods whose names sound clear but whose data flow is implicit through instance variables.
Signatures communicate less by default
A Ruby method name and parameter list usually do not state concrete types, nilability, units, mutation, exceptions, or I/O. RDoc can combine source comments with RBS signatures, making prose and types complementary rather than competing.10
Framework control flow is distributed
Rails callbacks, concerns, validations, jobs, and DSL declarations can move cause and effect across files. At those seams, explicit contracts and higher-level tests often provide more value than additional private helper extraction.
A Ruby-specific stopping rule for extraction
- Extract when the new boundary has a stable name and contract that callers can use without reading its implementation.
- Extract when it owns a domain concept, policy, external integration, or independently testable calculation.
- Keep together when fragments share local state, loop context, ordering constraints, or one invariant.
- Reconsider when reading the parent requires immediately opening every private helper.
- Do not extract solely to satisfy a numerical linter threshold.
Ruby examples
Tiny methods, hidden coupling
The top-level method reads like a story, but the story is incomplete. Each helper mutates shared instance variables, so the reader must inspect all of them to learn the state machine.
class Checkout
def call
load_cart
calculate_totals
reserve_inventory
capture_payment
persist_order
end
private
def load_cart
@cart = Cart.find(@cart_id)
end
def calculate_totals
@subtotal = @cart.items.sum(&:price)
@tax = Tax.for(@subtotal, @cart.shipping_address)
@total = @subtotal + @tax
end
# Other helpers read and mutate @cart, @subtotal, @tax, @total...
end
This is the Ruby form of Ousterhout’s entanglement concern: method count increased, but the amount of knowledge a reader must load did not decrease.
A deeper boundary with local coherence
class Checkout
Result = Data.define(:order, :receipt)
def initialize(pricing:, inventory:, payments:)
@pricing = pricing
@inventory = inventory
@payments = payments
end
# Completes a checkout atomically from the application's point of view.
# Payment capture may succeed before persistence, so callers must supply
# an idempotency key that remains stable across retries.
def call(cart, idempotency_key:)
totals = @pricing.quote(cart)
reservation = @inventory.reserve(cart.items)
receipt = @payments.capture(
amount: totals.total,
idempotency_key: idempotency_key
)
order = Order.transaction do
Order.create_from!(cart:, totals:, reservation:, receipt:)
end
Result.new(order:, receipt:)
rescue
reservation&.release
raise
end
end
This method is longer than RuboCop’s default maximum, but it keeps the orchestration order, failure path, and compensation logic visible. The extracted collaborators are deep boundaries: pricing, inventory, and payment each hide a separate body of knowledge.
Comments that earn their maintenance cost
# Weak: narrates the next line.
retry_count += 1 # Increment retry count.
# Strong: preserves a non-obvious external constraint.
# The provider may redeliver an event for 72 hours. Keep the key forever;
# expiring it can produce a second charge after a delayed retry.
idempotency_keys.insert!(provider_event_id)
TDD and bundling as complementary modes
A practical Ruby team policy
Method policy
- Use RuboCop metrics as review triggers, not automatic refactoring instructions.
- A method may exceed the threshold when splitting it would obscure ordering, shared invariants, or failure handling.
- Prefer extracting domain objects and stateless collaborators over chains of private helpers sharing instance variables.
- During review, ask “what knowledge does this boundary hide?” before asking “how many lines is it?”
An example configuration, not a universal optimum:
Metrics/MethodLength:
Max: 15
CountComments: false
AllowedMethods:
- change # Rails migrations are declarative scripts.
Metrics/BlockLength:
Exclude:
- "spec/**/*_spec.rb"
Comment and API documentation policy
- No comments that merely translate syntax into English.
- Document public gem APIs, extension points, callbacks, side effects, exceptions, units, and retry/idempotency behavior.
- Explain workarounds with a version, issue link, and deletion condition.
- Keep comments close to the invariant they protect; test the invariant where practical.
- Use RBS for machine-checkable type shape and prose for semantics that types do not express.
Testing policy
- Every defect receives a regression test.
- Favor behavior-level examples over tests coupled to private method structure.
- Use request/integration tests for Rails boundaries and focused unit tests for complex policies.
- Permit short design spikes, but discard or rewrite them before merge.
- Make design review an explicit step inside red-green-refactor, not an assumed side effect.
Review checklist
| Question | Healthy signal | Warning signal |
|---|---|---|
| Can I understand the parent method without opening its helpers? | Mostly yes | Every helper must be inspected |
| Does the extracted unit hide meaningful complexity? | Stable contract, substantial implementation | One renamed expression or state mutation |
| Is important behavior absent from the signature? | Documented contract/invariant | Caller must read implementation or tribal knowledge |
| Do tests describe behavior? | Survive internal refactoring | Mirror private method layout |
| Did a metric cause the design? | Metric prompted discussion | Code split mechanically to turn CI green |
The argument in their own words
Each excerpt is intentionally short. Follow the source link for complete context.
“The best methods are those that provide a lot of functionality but have a very simple interface.”
John Ousterhout, repository discussion 1
“We both value decomposition.”
Robert C. Martin, repository discussion 1
“The maximum allowed length is configurable.”
RuboCop Metrics documentation 3
“Avoid methods longer than 10 LOC.”
Community Ruby Style Guide 4
“Focus on why the code is the way it is.”
Shopify Ruby Style Guide 5
“The goal of Ruby documentation is to impart the most important and relevant information in the shortest time.”
Ruby Documentation Guide 6
“Take very small steps.”
RSpec 7
“Testing is central to the development process.”
Rails Testing Guide 8
Sources
- John Ousterhout and Robert C. Martin: “A Philosophy of Software Design vs Clean Code” Primary source. Repository README containing the full discussion of method length, comments, PrimeGenerator rewrites, TDD, summaries, and closing remarks.
- Repository commit history Used to identify the latest visible repository update at review time: 15 April 2025.
-
RuboCop: Metrics cops documentation
Primary tooling documentation for
Metrics/MethodLength, including enabled status, configurable maximum, defaultMax: 10, andCountComments: false. - Community-driven Ruby Style Guide Community convention source for short methods, self-documenting code, rationale comments, comment upkeep, and refactoring rather than narrating bad code. See also Comments guidance.
- Shopify Ruby Style Guide: Comments Ruby production-style guidance emphasizing relevant context, synchronized comments, proper prose, and explaining why rather than obvious mechanics.
- Ruby core: Documentation Guide Primary Ruby project guidance on the goals, location, and style of documentation for core and standard-library classes, modules, and methods.
- RSpec: Behaviour Driven Development for Ruby Primary RSpec introduction describing failing examples, incremental implementation, small steps, and refactoring.
- Ruby on Rails Guides: Testing Rails Applications Primary Rails guide covering model, controller, integration, system, job, mailer, and other testing levels.
- Ruby syntax documentation: Methods Primary language documentation for method definitions, names, arguments, return values, visibility-related behavior, and endless method syntax.
- RDoc: Ruby Documentation System Primary RDoc documentation. Notes that Ruby, C, and RBS sources can contribute to generated documentation and that matching RBS declarations extend source documentation.
Accessed 13 July 2026. This document is an independent synthesis. It is not authored, reviewed, or endorsed by John Ousterhout, Robert C. Martin, the Ruby core team, RuboCop, RSpec, Rails, or Shopify.