Fixing Shotgun Surgery in Ruby
When making a single logical change requires you to modify code across many different classes or files
Refactoring isn’t about making things prettier. It’s about making future changes easier, safer, and faster. In any app, that’s a real win.
Today we're going to talk about one of the most common code smells I see in Ruby codebases: shotgun surgery and how to reduce the smell.
👃 What Is Shotgun Surgery?
Shotgun Surgery occurs when making a single logical change requires you to modify code across many different classes or files. It’s the opposite of clean design — instead of making a change in one place, you’re hunting through your codebase with a shotgun, scattering edits everywhere.
This usually happens when:
Behavior is in the wrong place
Data is spread across the system
Related responsibilities aren’t encapsulated together
Let’s see this in action with an example from a simple pet boarding system.
🐶 Our Pet Boarding Domain
Let's say we’re building an app that manages pets staying at a boarding facility. We have Pet
and BoardingReservation
models. Pretty straightforward.
Here’s v1 of our code:
This works, but it smells.
🚨 Smell: Shotgun Surgery
Time passes, and requirements change. Let’s say our product owner decides to:
Add a new kennel type for senior dogs
Change pricing for cat condos
Apply discounts for dogs under 10 pounds
Every time we tweak the logic, we have to change multiple methods — kennel_type
, daily_rate
, maybe even more (like feeding schedule logic elsewhere). Worse, this logic depends on Pet
attributes, but lives outside the Pet
class.
We’re doing surgery all over the codebase just to make one change. Shotgun Surgery.
Let’s fix it.
🔧 Refactoring with Move Function
and Move Field
We'll apply two classic refactorings from Martin Fowler’s catalog:
Move Function — Move logic to the class that owns the data it uses most
Move Field — Move data fields to the class where they belong
Step 1: Move kennel_type
to Pet
Since kennel_type
depends entirely on the pet’s species and weight, it should live in Pet
.
This is much better. Now kennel_type
is in one place.
Step 2: Consider moving daily_rate
too?
If we notice that pricing is entirely driven by the pet's kennel type — potentially and especially if that pricing varies by pet type over time — we might move daily_rate to the pet as well:
This keeps related knowledge — what kind of pet this is and how much it costs to board them — in one place.
✅ The Result
Now when we want to change how we assign kennels or how pricing works, we only touch the Pet
class. No more shotgun surgery.
💡 Better encapsulation of behavior and data
💡 Fewer ripple effects when making changes
💡 Cleaner, more understandable models
🧠 Lessons Learned
If a method mostly uses fields from another class, that’s a strong clue it belongs there.
Shotgun Surgery hurts because it increases the cost and risk of change.
You can often fix it with Move Function and Move Field, pulling logic back to the object that owns the data.
Refactoring isn’t about making things “prettier.” It’s about making future changes easier, safer, and faster — and in a boarding app (or any app), that’s the real win.