Development Methodologies for Growing Teams
As engineering teams grow, the mental models that served five people often stop working for forty. Rules and checks that were implicit when everyone could hold the entire app in their head need to be made explicit. One way to do that is by naming the development approaches we already use—sometimes without realizing it—and turning those unconscious habits into public maxims.
Early in a career, a methodology like Test Driven Development teaches you to work backwards from outcomes. Later, something like Sandi Metz's Rules pushes you to think carefully about the code you're writing, not just whether tests pass. When you step into a tech lead role, the problems become human. Codifying what leads to success—and examining what leads to failure—becomes part of the job.
The point of adding more models isn't to pile on acronyms. It's to surface the assumptions underneath everyday decisions and give teams a shared vocabulary for discussing them.
Grep Driven Development
Grep Driven Development is really about naming. In Ruby especially, expressive variable names are the norm, but good names do more than help future readers. They affect searchability, refactorability, and consistency across a codebase.
Consider a real example. A developer new to Shopify was building a service and needed to make it act as a job. A teammate pointed out they could just add include ActsAsBackgroundable—a module that adds enqueue methods backed by the service's call method. Problem solved, but searching the codebase for the word "Job" would never have surfaced this module. When job-like behavior doesn't include the word "job," discoverability suffers. The default in Ruby should be that jobs end in the suffix Job.
Searchability also feeds refactoring. Martin Fowler's "rule of three" suggests you shouldn't go looking for an abstraction until you have three instances of similar code. Refactoring after one duplication tends to produce a premature—and likely incorrect—abstraction.
But there's a subtler trap. Suppose you copy a common method into a new object and take the opportunity to rename it something shorter and more idiomatic. You might feel good about the improvement, but you've just made a future refactor harder. Someone searching for the original method name, like can_be_reviewed_by?, won't find your renamed version and the code will drift further. Incremental improvements aren't always improvements if they make later wholesale changes more difficult.
Consistency is another form of the same principle. Future developers need to make assumptions about where behaviors live—predictable code paths, file locations, and side effects. The practical conclusion is to be boring. If your app uses verbs like Creator, Transactor, or ThingDoer, stick with them. If it ends everything in Service, Job, or Validator, don't deviate just because it feels redundant. Be kind to whoever comes spelunking after you.
Copy/Paste Driven Development
Don't Repeat Yourself (DRY) is a common axiom, but premature abstraction has its own costs. As Sandi Metz puts it, "duplication is far cheaper than the wrong abstraction." What a Grep Driven mindset adds is the ability to mitigate the tech debt of duplication: when code is consistent and searchable, duplicated code is easier to find when the time finally comes for a proper abstraction.
That said, there's a real place for duplication. In front-end frameworks, infinitely extensible abstractions built on flags and conditionals can bloat your bundle size. If behavior is loaded on every page just to avoid repeating a few lines, it's arguable whether that's even an abstraction anymore.
Where that leaves us is what could be called Copy/Paste Driven Development. Copying and pasting carries a stigma—it implies laziness or "creating duplication." But it's a normal part of daily programming life, and it has important traits worth recognizing:
- It prioritizes efficiency.
- It is an exercise in humility.
- It requires sound engineering skills.
When speed wins
Copying and pasting is fast—really fast. Existing, working behavior that took months to build can be replicated in seconds. Lessons that were already learned don't need to be rehashed. There are legitimate conversations about trade-offs between tech debt, product roadmaps, and shipping, but the right call changes with the state of the organization. Copy/paste is a tool. Reach for it when you're short on time, short on context, or have a solid example to lean on. And like any tool, it isn't right for every situation. A wrench can hit a nail, but that doesn't mean it's good for either.
The humility check
There's often an urge to rewrite similar code from scratch instead of copying it—something like "I don't want to copy code, I want to make sure I understand it." Before going down that path, ask yourself some hard questions:
- Will anyone else understand your version with you?
- Is rewriting primarily a learning exercise for you, and if so, is that really the best use of your time?
- Are you sure you understand the problem better than the teammates who wrote the original code?
- While you were "just understanding it," did the team ship features using the existing code?
Trusting the work of teammates doesn't preclude criticism. It just raises the burden of proof before you decide to go your own way.
Writing code worth copying
Copy/Paste Driven Development is a skill on both sides. You should strive to write code that's copy/pasteable—code that follows your team's established patterns. WET (repeated code, as opposed to DRY) tests are often very copy/pasteable: they call out specific situations, test one thing at a time, and are easy to delete or modify when circumstances change.
You might worry that writing easily duplicable code means your app doesn't do much, or that you're drawing the wrong abstractions. But consider a low-traffic CRUD (Create, Read, Update, Delete) app. Different objects aside, request specs end up looking fairly similar: pass parameters, check validation states, verify you get a view (or JSON) or an error. Much of the behavior on the web is fundamentally the same, and the core abstraction—Model-View-Controller (MVC)—is already in place.
The goal is not blanket approval for copy and paste. Instead, treat it as a set of skills:
- Can you read code effectively to know what it's doing?
- Can you see how edge cases are handled and reason through whether your situation is genuinely different?
- Can you imagine the code you'd write and see if it differs meaningfully from what's already written?
- Most importantly, if you can do all of that, can you explain your process clearly enough that your teammates gain these skills too?
Analyzing when copying and pasting is sufficient turns out to be one of the best contexts for teaching the fundamentals of sound software design. Recognizing a viable solution when you see one—and knowing when to reuse it as-is—is itself a form of engineering judgment.
Why We Pick Up a Problem Matters
There’s a tight coupling between the shortcuts we take in code and the reasons we take on work in the first place. Ego Driven Development is about questioning the motivation behind writing code — specifically, why we’re choosing to solve a particular problem at all.
The Pull of the Flashy Problem
Rarely does anyone say, “I want to solve this so people know how awesome I am.” But the impulse is real, and many of us feel it without recognizing it. Green field work is exciting, and admitting that it’s exciting is fine. Wanting to conquer a gnarly problem and feel that sense of accomplishment is okay. What matters is the ability to humbly ask why.
When a piece of work looks complex, cool, or fun, consider:
- Is someone else better equipped to solve this in a timely or robust way? If so, pairing with them might be the better learning opportunity.
- Is there a teammate who’s been waiting for a chance to show career growth and would benefit from taking on the larger project? Pairing with them can serve as both a teaching moment and a chance for them to level up.
- Do I have a clever solution in mind? If so, write up a technical design document and request feedback. Sometimes the idea really is gold, and input from teammates will help confirm that. Otherwise, be boring and predictable.
The Value of a Win
We shouldn’t discount what it means for someone to get a win. The first time a developer uses xargs and feels like a wizard, that feeling builds confidence and makes them more comfortable contributing to discussions. The goal is to help people get those wins and foster a culture of humble teammates who want to keep learning and growing.
Psychological safety plays a big role here. In short, people are most effective when they feel safe — when they’re not afraid to push themselves because failure might be punished. Junior teammates should always feel willing to take a shot, celebrated when they succeed, and supported when they don’t.
Ways to build that culture:
- Make an environment where failure is okay, and where you fail as a team, not as individuals.
- Help senior engineers understand their job is more about teaching others how to solve hard problems and less about solving them.
- Encourage teammates to advocate for and push each other, amplifying skills so people feel enabled to take on new challenges.
Knowing You’re Good Without Being Arrogant
Sometimes there’s consensus about who should solve a problem — everyone thinks it, including the person themselves. That’s the hardest scenario to navigate. It’s possible to be great at something, know it, and still be humble, if the motivation is shared success rather than accolades.
Humility isn’t meekness; it’s self-honesty. It’s a right understanding of our relationship with others. You can know you’re good at something without believing it elevates you above or diminishes anyone else. When the thrill of cutting request times by 50% feels no different from a new engineer’s excitement at writing their first multi-pipe bash script, that’s a good place to be.
Humility also shows up in how we approach boring work. If your enthusiasm for wrangling YAML files matches your enthusiasm for creating a new project from scratch, your motivations are likely in the right place. We shouldn’t try to prevent good engineers from doing good engineering well. Instead, focus on collaboration — share the burden of large failures, amplify small successes, and encourage humble spirits. That builds teams that act with discipline, encouragement, and efficiency.
When Rule-Following Becomes a Crutch
Rules and guidelines are part of everyday development — Sandi Metz’s Rules, The Twelve Factor App, a collection of Rubocop rules. The problem is we often inherit them without context, and our relationship with them shifts throughout our careers.
Early on, sticking to rules can be a way to avoid critical thinking. Quoting them to new engineers might feel like good teaching, even when you don’t yet have the full context yourself — perhaps using them to sound like a good teacher while feeling lost internally. Later, with a bit more experience, comes reckless abandon, ignoring most rules entirely. After getting burned by that recklessness, a senior engineer often returns to enforcing the rules with a firm, “they’re there for a reason!”
In all those phases, the lack of context around rules leads to what we can call Stickler Driven Development: do what the rules say because the rules say it. Even Sandi Metz’s rule zero — “You should break these rules only if you have a good reason or your pair lets you” — gets lost in the noise.
“Never and Always” vs. “It Depends”
Some rules don’t literally say “never” or “always,” but the sentiment is there. And it’s easy to counter everything with “it always depends.” Both extremes miss the point. The “ruleness” of a rule obscures the “whyness” behind it. There may be a rule against inventing words like “whyness,” but if the goal is successful communication and a bit of levity, why not use it?
The point of “never” or “always” is to provide a safe default for unknown situations or foggy-brain days when sticking to proven methods is the best move. The point of “it depends” is that you, as the engineer facing the problem, are best positioned to judge how it needs to be solved. But when “it depends” becomes just as absolute, you miss key nuances. It’s almost always true that edge cases exist — in code and in human endeavors. Yet “it depends” can become a justification for ignoring the rules that serve us well most of the time — and for the bad kind of Ego Driven Development.
What the Rules Are Really About
Consider a common Ruby convention: methods shouldn’t have “or” in their names. Take a contrived service object with a method that saves an item if it’s saveable, otherwise emitting it on a Kafka stream.
That method has an “or” in it. To follow the rule, the first move might be renaming the method to get rid of the word — technically satisfying the rule without addressing why it exists. The real goal isn’t about the word “or” at all; it’s about unnecessary conditionals and logic branching — the wrong abstraction at the heart of Grep Driven Development.
If we remove the conditionals and the elsif, the question becomes whether we’re actually done. Taking rules at face value misses that “or” and conditionals aren’t inherently bad. What we’re really after is the Single Responsibility Principle (SRP). Unnecessary complexity and incorrect coupling are the actual problems.
A better approach might be to pass persistence logic to the item itself. Let the item implement a persist method however it sees fit. The original logic then collapses to a single call on the item, and we see the original method was entirely unnecessary.
That’s a contrived example, but the solution may not be obvious to someone newer to the codebase or the language. Hearing about SRP or the “or” rule isn’t the same as internalizing the tools to use both rules effectively. Understanding the why behind a rule also tells you when it makes sense to break it.
Consider Rails’ upsert database method. It updates a record if it exists and creates one if it doesn’t — one thing or another. Yet it replaces a pattern duplicated across thousands of codebases of checking existence before creating or rescuing from database errors. Maybe the single responsibility is getting information into the database. Or maybe it’s fine to break the rules when the reasons are clear and the benefit is real.
Do the Human Thing
There was a time when I’d recently learned about metaprogramming, tried to use it for everything, and then learned why that’s bad. Not long after, a PR came in from a very seasoned Ruby developer for some workflow management. The workflow had methods called in a specific-but-varied order based on a status field. Complexity was too high to be captured purely in the status, so there was also a failed_step column representing the method that failed.
The resulting code used send to invoke a method based on that column. With my recent lessons fresh in mind, I proudly commented that using send is bad — because it is, for reasons. Mic drop.
My teammate essentially replied, “Yes, that’s usually true. But here, send provides far more clarity than any alternative.” They were right. Other approaches meant a large case statement, extra database objects, liberal constantize, or complex serialization. Given the constraints, this code was the least amount and, more importantly, easy to reason about.
Yes, it bent some Grep Driven Development ideas — but, as always, it depends. Searching for the method name replaced by send would find it in only one file, the exact one being edited. Tracing the code path from resume shows exactly what happens. Searchability was there, and consistency or refactoring concerns didn’t apply to a single class.
Rules about metaprogramming being bad are mostly about the human side. The computer doesn’t care. We write code to be easy for people to read and understand — that’s what makes a codebase nice to work with. My teammate’s argument was to do the human thing. Behavior-driven and test-driven development are all agreed-upon approaches so we can understand each other better. We want to write code that our future selves can understand — maybe we need Time Travel Driven Development.
Making the Implicit Explicit
The four approaches discussed here—Grep, Copy/Paste, Ego, and Stickler Driven Development—are not formal methodologies in the strict sense. They are practical tools and habits that most developers already use, often unconsciously. The value in naming them is to bring them into conscious awareness so they can be deliberately applied, adjusted, or discarded when the situation calls for it.
Naming these approaches serves a practical purpose: it reduces the cognitive load on teammates. When everyone shares a vocabulary for these patterns, discussing trade-offs becomes easier. Developers should reflect on how, when, and why they copy and paste code, rather than treating it as a default action without regard for whether it is the best fit. Likewise, it is worth recognizing moments where teaching and learning can happen naturally within the flow of real work, and being intentional about why rules exist and when breaking them is justified.
If these practices are ultimately about the human side of software development, a simpler framing may help internalize the underlying motivation:
Human Driven Development.
Further Reading
- Shopify Engineering careers
- Digital by Default
- Shitlist Driven Development by Simon Hørup Eskildsen
- The Rule of Three
- The Wrong Abstraction by Sandi Metz
- Sandi Metz Rules for Developers by Caleb Heart
- Don’t Repeat Yourself
- Bikeshedding
- WET Code
- Psychological Safety by Amy Edmondson
- The Five Keys to a Successful Google Team
- The Twelve Factor App
- Single Responsibility Principle



