Rails uses inheritance and mixins heavily to make methods appear throughout your application. This can be confusing for JavaScript developers used to explicit imports. While Rails framework code uses inheritance heavily, thoughtbot recommends your application code prefer composition. This makes your code easier to test and makes dependencies visible.
Why Composition Matters: Testing
The primary benefit of composition is easier testing. You can inject test doubles instead of real objects, making your tests fast, isolated, and free from external dependencies.
With implicit dependencies (inheritance):
# Inheritance example - requires database and complex setup
class WelcomeNotifier < BaseNotifier
def notify(user)
send_notification(user, welcome_message)
log_notification(user)
end
end
# Test has to deal with the whole inheritance chain
RSpec.describe WelcomeNotifier do
it "sends welcome notification" do
user = create(:user) # Requires database
notifier = WelcomeNotifier.new
# Have to mock methods from parent class
allow(notifier).to receive(:send_notification)
allow(notifier).to receive(:log_notification)
notifier.notify(user)
expect(notifier).to have_received(:send_notification)
end
end
With explicit dependencies (composition):
# Composition example - dependencies are explicit
class WelcomeNotifier
def initialize(mailer:, logger:)
@mailer = mailer
@logger = logger
end
def notify(user)
@mailer.send(user, welcome_message)
@logger.log(user, :welcome_sent)
end
private
def welcome_message
"Welcome to our app!"
end
end
# Test is simple and fast - no database needed
RSpec.describe WelcomeNotifier do
it "sends welcome notification" do
mailer = double("Mailer")
logger = double("Logger")
user = double("User")
notifier = WelcomeNotifier.new(mailer: mailer, logger: logger)
allow(mailer).to receive(:send)
allow(logger).to receive(:log)
notifier.notify(user)
expect(mailer).to have_received(:send).with(user, "Welcome to our app!")
expect(logger).to have_received(:log).with(user, :welcome_sent)
end
end
The composition version uses test doubles instead of real User objects. No database needed. Mailer and logger are fakes, not real services. Tests run in milliseconds, not seconds. You can see exactly what dependencies the class needs, and tests only check WelcomeNotifier logic without touching anything else.
What is Composition?
Composition means building complex functionality by combining simple, independent objects together instead of inheriting functionality from parent classes.
Building with LEGO blocks vs. nested Russian dolls:
- Inheritance (Russian dolls): Each doll is contained within a larger doll. You inherit everything from your parent, and to understand the smallest doll, you have to open all the outer ones.
- Composition (LEGO blocks): You snap independent pieces together. Each piece is distinct and clear about its purpose.
In JavaScript terms:
// INHERITANCE - you get everything from the parent
class Animal {
eat() { }
sleep() { }
breathe() { }
}
class Dog extends Animal {
bark() {
this.breathe(); // Where's breathe()? Inherited from Animal
}
}
// COMPOSITION - you explicitly combine pieces
class Dog {
constructor() {
this.lungs = new Lungs(); // Clear: Dog HAS lungs
this.stomach = new Stomach(); // Clear: Dog HAS a stomach
}
bark() {
this.lungs.breathe(); // Clear: calling breathe() on the lungs object
}
}
With inheritance, Dog IS-A Animal (and gets all Animal’s methods). With composition, Dog HAS-A Lungs and HAS-A Stomach (and uses their methods explicitly).
A Practical Example You Already Know
If you’re coming from Ember, Angular, or NestJS, the inheritance patterns in Rails will feel familiar—those frameworks use similar approaches. This article focuses on modern React/Vue/Svelte developers who are used to explicit composition patterns.
You’ve been using composition in JavaScript:
// COMPOSITION - passing dependencies in
function UserProfile({ apiClient, router }) {
const [user, setUser] = useState(null);
useEffect(() => {
apiClient.fetchUser().then(setUser);
}, []);
const handleClick = () => {
router.navigate('/home');
};
return <div onClick={handleClick}>{user?.name}</div>;
}
// Usage - you can SEE what this component needs
<UserProfile
apiClient={myApiClient}
router={myRouter}
/>
React has its own “magic"—useState tracks state somehow, useEffect manages side effects, and the compiler optimizes things under the hood. But you can see apiClient and router in the props, so you know immediately what this component depends on.
Compare that to if React used inheritance:
// INHERITANCE - methods appear from nowhere
class UserProfile extends MagicalReactComponent {
componentDidMount() {
this.fetchUser().then(user => { // Where's fetchUser()? Magic parent class?
this.setState({ user });
});
}
handleClick = () => {
this.navigateTo('/home'); // Where's navigateTo()? Magic parent class?
}
}
See the difference? With inheritance, you don’t immediately see what dependencies the class needs. Yes, you can find method definitions with "Go to definition” or by searching—but that’s not the point.
The issue is understanding code structure at a glance:
- With composition: Dependencies are visible in props/constructor. You see
{ apiClient, router }and immediately know what this needs. - With inheritance: Dependencies are hidden in parent classes. You have to hunt through the inheritance chain to understand what’s available.
Rails framework uses inheritance (you’ll have to work with that), but thoughtbot recommends composition for your own code because dependencies are clearer and testing is easier.
The Problem with Deep Inheritance in Your Rails Code
When you build features using inheritance, you recreate the same “where does this come from” problem:
# Implicit dependencies - methods come from parent
class BaseReportGenerator
def fetch_data
# ...
end
end
class UserReportGenerator < BaseReportGenerator
def generate
data = fetch_data # Where's fetch_data? Have to check parent class
# ...
end
end
class AdminReportGenerator < BaseReportGenerator
def generate
data = fetch_data # Same problem - not obvious where this comes from
# ...
end
end
When reading AdminReportGenerator, you have to hunt through parent classes to find fetch_data.
The Solution: Composition in Rails
Make dependencies explicit by passing them in:
# Explicit dependencies - you can see exactly what's being used
class ReportGenerator
def initialize(data_fetcher)
@data_fetcher = data_fetcher # This object needs a data_fetcher
end
def generate
data = @data_fetcher.fetch # Calling method on the data_fetcher we passed in
# ...
end
end
# Usage - dependencies are visible
user_generator = ReportGenerator.new(UserDataFetcher.new)
admin_generator = ReportGenerator.new(AdminDataFetcher.new)
Real-World Rails Examples
I use “plain Ruby classes” in these examples to mean classes that accept dependencies via their constructor. If you’re from Angular or NestJS, you might call these “services.” In Rails, there’s debate about “Service Objects” as a pattern. I’m simply showing dependency injection and composition here. Call it whatever works for your team.
Example 1: Plain Ruby Classes with Composition
Implicit dependencies (Inheritance):
class BaseNotifier
def send_notification(user, message)
# email logic
end
def log_notification(user)
# logging logic
end
end
class WelcomeNotifier < BaseNotifier
def notify(user)
send_notification(user, welcome_message) # Where's send_notification?
log_notification(user) # Where's log_notification?
end
end
Explicit dependencies (Composition):
class WelcomeNotifier
def initialize(mailer:, logger:)
@mailer = mailer # Dependencies injected via constructor
@logger = logger
end
def notify(user)
@mailer.send(user, welcome_message) # Calling method on injected mailer
@logger.log(user, :welcome_sent) # Calling method on injected logger
end
private
def welcome_message
"Welcome to our app!"
end
end
# Usage shows all dependencies
notifier = WelcomeNotifier.new(
mailer: EmailMailer.new,
logger: NotificationLogger.new
)
notifier.notify(user)
Example 2: Data Processing Pipeline
Implicit dependencies (Inheritance):
class BaseDataProcessor
def validate_data(data)
# validation
end
def transform_data(data)
# transformation
end
def save_data(data)
# persistence
end
end
class UserDataProcessor < BaseDataProcessor
def process(raw_data)
validate_data(raw_data) # Where do these come from?
transform_data(raw_data) # Have to check parent
save_data(raw_data) # Not obvious
end
end
Explicit dependencies (Composition):
class DataProcessor
def initialize(validator:, transformer:, repository:)
@validator = validator # Dependencies injected
@transformer = transformer
@repository = repository
end
def process(raw_data)
@validator.validate(raw_data) # Calling method on injected validator
transformed = @transformer.transform(raw_data)
@repository.save(transformed)
end
end
# Create specialized processors by composing different components
user_processor = DataProcessor.new(
validator: UserValidator.new,
transformer: UserTransformer.new,
repository: UserRepository.new
)
admin_processor = DataProcessor.new(
validator: AdminValidator.new,
transformer: AdminTransformer.new,
repository: AdminRepository.new
)
Example 3: Authentication With Composition
A BaseController between ApplicationController and your feature controllers is a valid Rails pattern that many apps use successfully. This example shows an alternative approach using composition, not a critique of BaseController.
Implicit dependencies (Additional inheritance layer):
class BaseController < ApplicationController
def current_user
# auth logic
end
def authorize!
# authorization logic
end
end
class PostsController < BaseController
def create
authorize! # Where's this from? BaseController? ApplicationController?
# ...
end
end
Explicit dependencies (Composition):
class PostsController < ApplicationController
before_action :setup_auth_service
def create
@auth_service.authorize!(current_user, :create_post) # Calling method on injected service
# ...
end
private
def setup_auth_service
@auth_service = AuthenticationService.new # Dependency created in setup
end
def current_user
@auth_service.current_user(session)
end
end
Benefits of Composition
Composition makes testing dramatically easier. Inject test doubles instead of real objects, and tests run in milliseconds without databases or external services. Dependencies are clear from the initialize method. You can search for class names and find all uses in your codebase. Swapping implementations doesn’t require changing inheritance hierarchies. Methods are either defined in the class or called on an injected dependency, so there’s no mystery about where they come from.
When to Use Inheritance vs. Composition
Use Inheritance when:
- Working with Rails framework classes (ApplicationController, ApplicationRecord)
- Working with Rails-ecosystem libraries that expect inheritance (Pundit policies, ViewComponents, Interactors, notification gems, etc.)
- Creating genuine “is-a” relationships (an Admin IS-A User)
- The conventions of the framework/library expect it
Prefer Composition for:
- Building your own business logic
- Making dependencies explicit and visible
- Writing code that’s easier to test in isolation
- When you need flexibility to swap implementations
Key insight for JS developers: You’ll use inheritance when working with Rails and its ecosystem (you don’t have a choice). Prefer composition for your own application code because it’s closer to what you’re used to in modern JavaScript and makes testing easier.
Why This Matters for Rails
You can’t change Rails framework code - you’ll always have to deal with methods appearing from ApplicationController or ApplicationRecord. That’s just how Rails works.
But you CAN control your application code. When building your own features, use composition to make dependencies visible. Your tests will be faster, your code will be clearer, and you won’t have to trace through inheritance chains to understand what a class needs.
The Key Takeaway
Rails framework uses inheritance and mixins, so you have to learn where things come from. Your application code can use composition to make dependencies explicit. You only fight with “where does this come from” when dealing with Rails itself, not your own code.
This is a core thoughtbot principle: embrace Rails conventions for framework code, but write clear, explicit application code.
Practical Tips for Applying Composition
Inject dependencies in initialize to make what your class needs explicit. Use keyword arguments like initialize(mailer:, logger:) to make dependencies clear. Keep classes small. If you need many dependencies, your class might be doing too much. Extract business logic into plain Ruby classes that use composition. Test with doubles since composition makes it easy to inject test doubles for fast tests.
Conclusion
You already know composition from JavaScript - React props, dependency injection, functional programming. Rails code works the same way when you choose composition over inheritance chains.
Start with simple classes that take dependencies in their constructor. Test them with doubles. When you read code six months later, you’ll see exactly what each class needs without tracing through parent classes.