Ruby language notes

Private and protected methods in Ruby

A short guide to Ruby method visibility, explicit receivers, protected comparison helpers, and why private is usually the simpler default.

Visibility as collaboration rule

Ruby method visibility is less about security and more about collaboration. It tells other code which messages are part of an object's public contract and which messages are implementation details.

public methods are the interface. private and protected methods are ways to reduce that interface. The smaller the public surface, the easier an object is to change without breaking callers.

The usual default is simple: make methods public only when other objects should call them.

Private methods

A private method cannot be called with an explicit receiver in ordinary Ruby syntax. Inside the object, the call is made as a bare method call, which means "send this to self."

This rule pushes private methods toward helper behavior for the current object. They can clarify a public method by naming steps without advertising those steps as part of the object's API.

If another object needs to call the method, that is a design signal. Either the behavior belongs in a public method, belongs in another object, or is not actually a private helper.

Protected methods

Protected methods are more specialized. They can be called by instances of the same class or hierarchy, including with an explicit receiver. This makes them useful for peer-to-peer comparisons.

A classic example is comparing internal state between two objects of the same type. The outside world should not read the state directly, but peer instances may need it to implement equality, ordering, or compatibility checks.

Protected is less common than private because this peer-access pattern is less common. When in doubt, private is usually the simpler choice.

send and public_send

send can bypass visibility and call private methods. That is sometimes useful for metaprogramming or tests, but it should be treated as an escape hatch. It ignores the collaboration rule the object declared.

public_send respects public visibility. It is safer when method names come from dynamic input or when code should only call the public API.

The distinction is practical. If code uses send casually, private methods stop being private in design terms, even if Ruby still labels them that way.

Default rule

Start with a small public interface. Move helper methods below private. Reach for protected only when objects of the same type need to inspect each other.

Visibility will not save a bad object model, but it does make intent reviewable. A reviewer can see which messages are meant for the world and which exist only to keep the object readable.

That is enough. Ruby visibility is a lightweight design tool, not a fortress.

Testing visibility

Tests should usually exercise public behavior rather than private methods. A private helper is an implementation detail, and tests that call it directly can freeze the internal shape of the object.

There are exceptions during difficult refactors, but they should feel temporary. If a private method needs direct tests for the long term, it may be a separate object trying to get out.

A small visibility example

The difference between private and protected is easiest to see when one object needs to compare itself with another object of the same type. A private helper is only for the receiver's internals. A protected reader can be called by sibling instances.

class Bid
  include Comparable

  def initialize(amount_cents)
    @amount_cents = amount_cents
  end

  def <=>(other)
    amount_cents <=> other.amount_cents
  end

  protected

  attr_reader :amount_cents
end

Here amount_cents is not public API for the rest of the application, but another Bid can use it to implement comparison. If no sibling object needs that access, private is the simpler default.

class Invoice
  def total_cents
    line_items.sum { |item| item.quantity * item.unit_price_cents }
  end

  private

  def line_items
    @line_items ||= []
  end
end

That second helper exists only to keep the class readable. There is no reason for another invoice to call it, so making it protected would advertise a relationship that does not exist.

Public API pressure

Visibility is also documentation for future maintainers. Public methods invite callers. Once other objects depend on a method, changing it becomes a compatibility concern. Private methods keep that pressure lower because they tell readers the method is part of the implementation, not the object contract.

The practical habit is to start narrow and widen only under pressure from a real collaborator object. If tests need to call a private method directly, that may be a temporary refactor aid, but it can also mean the helper wants to become a separate object with its own public behavior.

That framing also makes reviews easier. Instead of debating visibility as style, ask who the intended caller is. If the only caller should be the object itself, private is the clearest answer.

The answer can change later, but widening visibility is easier than recovering a method that became public by accident. That restraint prevents accidental framework design inside ordinary objects, and it keeps intent visible.

Sources

  1. Ruby documentation, method visibility. https://docs.ruby-lang.org/en/master/syntax/methods_rdoc.html#label-Method+Visibility
  2. Ruby documentation, Object#send and Object#public_send. https://docs.ruby-lang.org/en/master/Object.html