YAGNI and DRY — the KISS of Death for Your Software Project
Learn why popular programming mantras like YAGNI, DRY, and KISS can be as destructive as they are seductive.
Software development is full of mantras that are chanted by developers of all levels as prima facie evidence that justifies and proves the rationality of their decisions.
Who amongst us hasn’t heard people or told people to YAGNI, DRY, or KISS? They’re popular mantras because, when followed properly, they work to guide and nudge thinking towards more effective development and programming.
However, they have a dark side.
Because these terms are intentionally vague and broad, they can be misapplied or over-applied in many situations. These memes are then used as justification to continue applying the wrong approach to a problem.
They’re hard to argue against because philosophically these generalized statements are correct. We want to believe them. We have to believe them. To not believe them makes one feel as if they are not a developer. This makes them difficult to argue against, combined with the fact that they contain only the broadest of truths and are not applicable in every situation.
KISS — Keep it Simple, Stupid
Despite hurting feels across the world, KISS, or “keep it simple, stupid” is often used to promote simplicity and prevent the over-engineering tendencies many engineers have.
There might be a hundred ways to implement any one requirement. KISS implies that the simplest method is often the best in terms of complexity, speed, and cost.
For example — an engineer may need to implement a way to change the program configuration. They may be evaluating two different approaches: read from the database, or read from a file.
Developers apply KISS in this situation: avoid complexity and dependencies caused by the additional database tables by choosing the simpler method of reading from a file.
What’s wrong with it?
The dark side of KISS is that is can lead you down the path of the minimum, which if applied over time eventually results in subpar outcomes.
Let’s suppose a second requirement follows that the configuration needs to be changeable during runtime by internal employees.
A KISS-focused iteration after this could be to give internal administrators access to the server to change the configuration file. However, this goes down a path that results in more ineffective results than you would have had you taken the slightly more complex option of putting it in a database table.
KISS is also often used to defend under-developed solutions and gloss over implicit or unspoken requirements. In the example above, one of the unspoken requirements is that we don’t want the server to go down if we changed the configuration. However, because we’ve gone down the route offered by KISS, this constraint is not even mentioned or addressed.
Sometimes situations involve real complexity, and in these situations, KISS can dangerously scope out important considerations.
YAGNI — You Aren’t Gonna Need It
YAGNI is often used to prevent engineers from over-engineering their solutions in an effort to address situations or requirements that don’t currently need to be addressed.
Let’s face it — engineers have a tendency to overbuild for use cases they will never see, or inappropriately delay value delivery for minimal gains in architectural and code cleanliness — code-golf is a thing, as are ivory towers.
YAGNI is the mantra that acts as the counter-balance to the path of infinite “what if?” questions that engineers can travel down. It prevents scope creep that comes from attempting to address the problems of an uncertain future that are better left to be solved for when they actually occur (if they ever).
What’s wrong with it?
YAGNI has a dark side. It is often used by engineers as a reason for under-engineering in a misguided attempt to justify sloppy work with nebulous promises of unverifiable gains in development speed.
How do you tell if it is being misapplied? When YAGNI is used as the reason for NOT doing something, it’s time to ask yourself some questions:
- Is it being used to avoid solving a problem that is inherent to the domain?
- Is it being used to avoid developing something inherently foundational to the product or software (eg. authorization for web applications)?
- Are speed benefits from avoiding the additional work being tracked to ensure it actually leads to faster development?
- Is the cost of doing it or implementing it actually as costly as believed?
YAGNI can be a powerful guiding principle when applied judiciously. However, it can also be used as a crutch to excuse sloppy development and low quality work in the name of faster delivery speeds that never materialize.
A tale of a highly detailed project
I once worked on a project that had almost no abstractions. Instead, every line of code was at the lowest possible level of detail. Functions were thousands of lines long, forcing you to read every line to understand what it was doing. There were few classes or reusable libraries.
The team was hesitant to create classes or even move code into functions. They believed it was over-engineering and yelled “YAGNI”: why move code into a function when it wouldn’t be called by anything else? Why create abstractions we didn’t need when copy-pasting was so much faster?
I had to remind them that engineering was about developing primitives and abstractions that hide the details so that you can focus on more important concepts and ideas.
Software is founded on abstractions: from on/offs to bits to registers to memory management to instructions to code to functions to classes to libraries to programs to services. Without these and other abstractions, our minds are forced to contend with details that aren’t relevant for what we’re trying to do, greatly distracting from solving the real problem at hand.
More often than not, these details don’t matter.
An alternative — You Are Gonna Need It
Spend a bit of extra time thinking about and modeling the technical and business domain. If it is some common function such as logins, file downloads, or auditing, think about how you would implement it, and then implement something minimal that will move you towards the ideal solution.
Remember — the value of the plan is in the planning. Forcing yourself to think about all of the “What Ifs?” will help ensure you don’t paint yourself into a corner that will prevent the code from evolving to support changing requirements. It doesn’t mean you have to everything at once — you can still ensuring you move in small, incremental steps.
This will save a tremendous amount of resources in the long-run, especially if it is something foundational to future development efforts, such as code related to UI components, design systems, authorization, endpoints, or data layers. These are things that are common and highly likely to be needed as the product evolves.
If you solve a problem just once and write the solution in a way where you can use it anywhere else you encounter the problem, you have effectively saved an exponential amount of time from future development.
DRY — Don’t Repeat Yourself
DRY is all about not doing the same thing over and over and over and over and over and over and over. It encourages developers to think about commonalities and not just robotically copy-paste functionality.
By emphasizing centralization of shared functionality, teams can more easily keep code volume in check, preventing a cascading propagation of changes should a commonly copied piece of code require modification.
What’s wrong with it?
DRY casts a terrible spell on developers who take it too far and centralize code that has the same form but not the same purpose. They take code that might be structurally similar and tie use cases together, causing a change in one to lead to an unintended or undesired change in the other. I see this often in React/Redux developers who use Redux to avoid having to think about state management.
This unintended globalization of previously localized data can lead to a lot of negative consequences.
A tale of a User
In one project I worked on, we had a a list of users on the front-end. The developers were judiciously following DRY, and placed these values in a centralized cache for use by all of the other pages, including two in particular: a user list page and a user lookup search.
This proved to be a mistake. Even though both components happened to use a list of users, they had completely different requirements and sources of change for that data:
- The user list page wanted many additional details in the payload, whereas the user lookup wanted just the name and ID.
- The user list page contained PII such as full names and email addresses, whereas the user lookup only contained minimal public information.
- The user list page was manually editable and sortable, whereas the lookup was not.
Because these two shared the same centralized data store, it wreaked havoc on the front-end. Sometimes data was missing. Other times items came in out of order. Sometimes using the user lookup caused the user list to disappear. It was a mess.
The sources of truth for these pieces of data should have been completely different, but they had been DRY’d up and combined.
An alternative — Do Repeat Yourself
Whenever you store or use a piece of data or write some code, always ask yourself the following questions:
- What are the reasons this code or data may change, and do those reasons apply to this situation?
- What use cases are leveraging this code or data, and is this shared piece generic enough to suit both cases without impacting either one?
- What stakeholders would request changes to this piece of functionality, and could they simultaneously both be right and disagree?
If you identify that the form may be similar but the purposes or reasons different, give yourself permission to repeat yourself.
Be judicious in your application of mantras. While they can help guide thinking and are useful rules of thumb, they were never intended to be dogmas.
Don’t be a lazy thinker. Always be cognizant of the “Why?” behind what you are doing, and know when to break the rules.