Fix the Gun, Then Write Less Clever Code
Hatched by John Smith
May 09, 2026
9 min read
3 views
84%
The real bottleneck is usually not the code
What if the fastest way to improve a codebase is not to write better functions, but to stop needing so many of them? That sounds almost lazy, until you notice how much engineering pain comes from repeating the same ceremony in slightly different forms: fetching an object, forwarding a method, wrapping a dependency, reimplementing a pattern that a framework already solved once. Many teams obsess over fixing individual bugs, when the deeper problem is that the system keeps producing bugs of the same shape.
That is the meaning hidden inside a deceptively blunt idea: if you keep shooting yourself in the foot, fix the gun. In software, the gun is not just a line of bad logic. It is the environment that makes bad logic easy, normal, and repetitive. The same instinct shows up in a very practical Ruby question: how do you get Rails style delegation in plain Ruby without dragging in a whole heavyweight dependency just for one convenience? The temptation is to solve the symptom directly. The better move is to ask whether the symptom is revealing a missing abstraction, or an abstraction that has become too expensive.
The deeper tension is this: should we reduce friction by importing more machinery, or by redesigning the machinery we already have?
The dangerous comfort of tiny repeated fixes
Every engineer has seen the same pattern in a codebase. A method is missing on a wrapper object, so you forward it manually. Another object needs the same behavior, so you copy the forwarding. A third place needs the same thing again, so you sprinkle in a helper or pull in a library. Each step feels reasonable because it solves the immediate issue with minimal disruption.
But repetition changes the nature of the problem. What began as a local convenience becomes a systemic tax. The team stops thinking in terms of what the program represents and starts thinking in terms of what methods need to be forwarded today. That is how foot shooting becomes institutionalized. The gun is not the bug in one method. The gun is the habit of making the same low leverage fix over and over.
This is where delegation is a perfect case study. Delegation is one of the cleanest ways to preserve boundaries in object oriented design. A wrapper can expose a focused interface while handing the real work to another object. Done well, delegation improves clarity. Done poorly, it becomes invisible plumbing that multiplies complexity. In Rails, delegation is often easy because the ecosystem already provides ergonomic tools. In plain Ruby, reaching for the same convenience can feel slightly annoying, and that annoyance matters. It forces a question that many teams avoid: is this convenience essential, or just familiar?
A lot of technical debt is built from unchecked familiarity. We see a pattern in one environment, assume it is the standard, then recreate it everywhere else even when the cost model has changed. The result is a codebase that looks elegant in isolated pieces and increasingly brittle in the aggregate.
Convenience is not free, it just hides its invoice
There is a seductive story engineers tell themselves: use the framework feature, save time, move on. Sometimes that is absolutely correct. But convenience features are never just convenience. They come with import cost, cognitive cost, coupling cost, and maintenance cost. Even the smallest helper can become a dependency wedge that quietly expands the surface area of your system.
That is why the desire to create a tiny, Rails like delegate utility in plain Ruby is more interesting than it first appears. It is not merely an implementation trick. It is a design stance: borrow the behavior, not the baggage. If you only need one small slice of a broader framework, pulling in the entire stack can be like installing an industrial kitchen just to make toast. Yes, the toast will work. But now you also own the industrial kitchen.
This is the same principle behind fixing the gun instead of the foot. If your team keeps producing the same class of mistake, do not just patch each wound. Look upstream. Maybe the interface invites error. Maybe the abstractions are too leaky. Maybe the dependency graph makes the wrong thing easier than the right thing. Often the cleanest fix is to change the shape of the system so the mistake becomes awkward to make.
A useful mental model here is the difference between local optimization and structural optimization.
- Local optimization says: “This line is ugly, let’s make it prettier.”
- Structural optimization says: “Why does this kind of ugliness keep appearing?”
That shift matters because software pain usually scales by pattern, not by event. One typo is a typo. A typo repeated across a hundred wrappers is a design failure. One missing delegate is an oversight. A recurring need for forwarding suggests that an object is pretending to be something it is not.
The best abstractions do not merely reduce code, they reduce the number of times humans have to remember to be careful.
That is why delegation is never only about code reuse. It is about shaping attention. A good abstraction removes a class of decisions from the developer’s daily life. A bad abstraction creates the illusion of simplicity while hiding a constant background burden.
The smallest useful abstraction is often the most powerful one
There is an important nuance here. The answer is not “never use libraries” or “always write your own helper.” The answer is to identify the smallest abstraction that solves the actual recurring pain. Too little abstraction leaves you in manual repetition. Too much abstraction buries a simple need inside an ecosystem you do not truly need.
That is why lightweight delegation in plain Ruby is a revealing example. The goal is not novelty. The goal is precision. If a handful of lines can give you the ergonomics of delegation without importing a large dependency tree, you have improved the system in a way that is both tactical and architectural. You have reduced friction without increasing the size of the gun.
This principle generalizes far beyond method forwarding. Consider these common engineering choices:
- Writing a tiny internal helper instead of introducing a broad utility library
- Creating a focused adapter instead of forcing a domain object to expose every underlying method
- Refactoring a repeated conditional into a small policy object instead of scattering branches everywhere
- Fixing a flawed interface instead of training everyone to use it carefully
Each of these is a way of saying: make the right thing cheap, the wrong thing inconvenient, and the system will get better without heroic discipline.
That last part is crucial. Many organizations rely on human excellence to compensate for bad systems. They hire sharp engineers and then ask them to remember dozens of conventions, work around awkward APIs, and mentally simulate fragile flows. This is unsustainable. Good systems do not depend on constant vigilance. They make unsafe paths harder to take.
If you zoom out, the relationship between delegation and “fix the gun” becomes clear. Delegation is not just a coding convenience, it is a way of deciding where behavior should live. A wrapper object that delegates carefully can preserve a clean boundary while still behaving naturally. A system that repeatedly forwards by hand is often telling you that the boundary itself is wrong or incomplete. In both cases, the key question is not “How do I eliminate this annoyance?” It is “What design decision is this annoyance trying to surface?”
A practical framework: symptom, leverage, and ownership
When a team keeps tripping over the same kind of problem, use this three part framework.
1. Symptom: what keeps happening?
Be concrete. Is it repeated delegation? Missing methods on wrapper objects? Too much framework coupling? Every fix begins with naming the recurring pain precisely. Vague frustration leads to vague solutions.
For example, if you keep writing boilerplate forwarders, the symptom is not “too much code.” The symptom is that your object model and your interface expectations are misaligned.
2. Leverage: what change prevents the next ten occurrences?
This is where you resist the urge to patch just one instance. Ask which change alters the slope of the problem. Could a tiny helper remove repetitive forwarding? Could an interface be narrowed? Could the wrapper become a true domain object instead of a pass through shell? The highest leverage move is often boring, but it changes the economics of every future decision.
This is the core of fixing the gun. Do not optimize the wound. Optimize the mechanism that keeps producing wounds.
3. Ownership: who should be responsible for the behavior?
Delegation forces ownership questions. If one object forwards to another, who actually owns the contract? Who is the source of truth? Who is allowed to change? The answer should be visible in the design, not only in the implementation. If the ownership is fuzzy, every delegation site becomes a future maintenance trap.
This is why the tiniest utility can be so useful when it is narrowly scoped. It can clarify ownership rather than obscure it. Instead of hiding the design under a giant framework, it encodes exactly the relationship you need and nothing more.
Good engineering is often the art of refusing to confuse familiarity with necessity.
That sentence is worth sitting with. Familiar tools feel safe because we know how to use them. But when we import them reflexively, we stop asking whether they still deserve their place in the architecture. The result is code that is easy to start and hard to evolve.
Key Takeaways
- Stop treating repeated mistakes as separate incidents. If the same kind of problem keeps appearing, there is probably a system level cause.
- Prefer structural fixes over repeated patches. Change the interface, abstraction, or dependency boundary so the mistake becomes harder to repeat.
- Borrow behavior before importing baggage. If you need one small framework like convenience, consider a focused implementation instead of a heavy dependency.
- Make the right thing cheap. Good design reduces the number of times developers must remember rules, conventions, or workarounds.
- Use delegation intentionally. Delegation should clarify ownership and boundaries, not become invisible boilerplate that masks a muddled object model.
The deeper lesson: software problems are often design problems in disguise
The smartest teams do not just fix defects faster. They get better at noticing when a defect is actually a symptom of a worse design choice. That is why the most valuable engineering instinct is not cleverness, but refusal. Refuse to accept recurring pain as normal. Refuse to normalize boilerplate when a small abstraction would do. Refuse to pay the maintenance cost of a giant tool when a tiny one solves the real job.
This does not mean becoming allergic to frameworks or libraries. It means understanding their role with adult precision. A framework is a gun with a design. Sometimes that is exactly what you need. Sometimes it is an overpowered mechanism that encourages the very mistakes you are trying to avoid. The mature move is not to worship the tool or reject it outright, but to ask whether it is making your intended behavior easier and your accidental behavior rarer.
In the end, fixing the gun is not about being defensive. It is about being honest. If your team keeps shooting itself in the foot, the answer is rarely “try harder.” The answer is usually: redesign the mechanism, reduce the temptation, and choose the smallest abstraction that makes the right path the natural one.
That is what elegant code really is: not code that looks clever, but code that makes future mistakes less likely to exist.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣