OUR THOUGHTSTechnology

Do YAGNI, KISS and DRY always provide better engineering and product development?

Posted by Davin Ryan, Ajay Blackshah . May 19.25

We are regularly reminded by content creators and experts that YAGNI, KISS and DRY reduce ‘dumb work’, are ‘need to know’ and ‘key software design principles’ to get the best out of your software engineering teams leading to better business outcomes.

Well, we agree with these positions but this blog isn’t just another article about that. We’re going to tell you what doesn’t get as much airtime as it deserves – the grey area in between – when and where you should break these rules and how to use YAGNI, KISS and DRY in a mature way to take your delivery teams to the next level.

Before we continue, let’s do a quick refresher on what these terms mean:

  • YAGNI – ‘You aren’t gonna need it’. Sometimes referred to as ‘don’t build for a future that may not exist’. Spend your resources on features your customer wants now, not what they ‘might’ want in the future. Rely on in-app feedback, prototypes or experiments (features you can toggle off when they are too unstable) to validate customer demand
  • KISS – ‘Keep it simple stupid’. Simpler, more intuitive code is easier to ‘reason about’, ‘find things’, ‘add new features’ and is a ‘pleasure to work on’. This leads to higher morale which leads to higher productivity which leads to faster business feature delivery. Most importantly, you must also learn to remove unused code or features based on customer feedback. De-clutter and guide your users through your interfaces to increase customer satisfaction and adoption
  • DRY – ‘Don’t repeat yourself’. Eliminate repetitive logic, code and documentation. Centralise knowledge instead, update in one place rather than many. This creates consistency and can lead to easier maintenance and team efficiency. Use your code as its own documentation, rely on re-usable modules or higher-order components/functions to add common behaviour to existing components/functions

‘Sensible defaults’ instead of ‘strict processes’

So what’s the problem? This all sounds pretty good. To understand why we cannot always rely on YAGNI, KISS and DRY, we need to look at how these things appear in real life, where they come from and who is accountable (probably the most important reason).

Software engineering, in many domains, is not yet a commodity but still a craft, not unlike those ancient swordsmith guilds whose craftsmen hammered out swords on anvils. Each sword is unique, just like each piece of software. In an attempt to move from a craft to something that can scale and compete with other businesses more aggressively in the marketplace, many senior engineers are strongly encouraged to share their knowledge. They are encouraged to do this in such a way that it can be replicated without their regular direct intervention or oversight. We often refer to this as ‘processes’ which come in the form of practices, standards and principles. For example, in addition to those practices already mentioned, they may take the shape of – organise your work using Jira with this company-wide template, use REST APIs, write tests, use this validation framework, use this caching framework etc…

There is an ironic contradiction here to which we must have some sort of balance to avoid killing innovation.

By having more processes, we are moving towards constraining how engineers can think about and solve a given problem. This is great because solving problems is hard and takes much time and experience. So any process that can increase engineering productivity and, more specifically, junior engineering productivity, is going to lead to greater efficiency and better business outcomes. We refer to this as a focus on execution over innovation.

However, not every problem is the same and shouldn’t be solved in the same way and many companies take processes so literally that they become the rule instead of a guide. We propose using processes as sensible defaults, a starting point from which you can diverge when the need arises based on the problem you are solving. This is the ‘balance’ mentioned earlier, which will ensure you’ll get the best mix of execution and innovation. But what does that look like with YAGNI, DRY and KISS?

A mature and practical approach to YAGNI, DRY and KISS

The following distils YAGNI, DRY and KISS down to simple, memorable statements that should help you know when to apply these practices to get the best results using real-world examples.

YAGNI – Use for code that has low technical inertia to mitigate significant delays

Code tends to fall into two high-level categories, high and low inertia. The difference is one of scale. The more you reuse a code module, the more dependencies will exist between code modules, making it harder to make changes without breaking those other code modules. The difference is analogous to steering a container ship versus steering a small yacht.

The container ship has to plan ahead and can take 15-20 minutes to execute this manoeuvre, whereas the more agile yacht can do this in seconds and change its mind without losing much time if it was a mistake. Business logic is typically made up of low-inertia code, for example, searching for a catalogue item, ordering a specific product or getting feedback through a survey.

Base or platform type code is typically made up of high-inertia code, such as logging, authentication and caching. It is a good idea to use the YAGNI principle in software development for low-inertia code and ignore it for high-inertia code, as this requires a lot more planning due to its risk and impact on your team and others.

For example, you’ve just added a new authorisation framework to your service and have proudly provided a very generic interface allowing it to be used in many different scenarios. You currently have very basic requirements for it, namely, the ability to ensure a user has a specific activity or permission to execute a particular function/behaviour.

The other teams are very impressed and adopt your authorisation framework. You notice that almost 20 services in your company are now using your framework and you feel pretty good about this. However, after six months of development, someone comes up to your desk and says, ‘I like your authorisation framework but my service manages user profiles and I need the ability to check if someone not only has the right activity, but is also the owner of their profile when making changes to their profile preferences’.

You think about this and find out that there is no way your authorisation framework could possibly handle this without rewriting it from the ground up. It just was never built with this type of demand in mind and you sadly have to inform that person that it will take at least one week of work and even more time to communicate and share with the other teams already consuming it and address any issues leading to delays and many teams not meeting their sprint goals.

In many cases, it takes much longer than this. Authorisation has what we call ‘high technical inertia’. It is very common/generic logic that can be scaled up to many services. In this case, it would have been better to think ahead about what future functionality the company’s customers might have needed to avoid a significant amount of rework and delays.

DRY – Use for code that has low rates of change and high rates of reuse to mitigate brittleness

Consider the oversimplified diagram below in which circles indicate code modules. The pink modules are unique and reuse functionality from the orange modules, which are not unique but have the same functionality.

Migration to star topology and a central module due to code reuse
}

Therefore, pink modules have a ‘dependency’ on orange modules. Refactoring to a star topology, where we have a single orange module, will make sense most of the time. When we make changes, we do them in one place and all pink modules benefit immediately. However, the ‘dark’ side to this approach is brittleness. The more ‘dependencies’ we have, the more brittle our code is because every change to an orange module risks breaking the functionality of all pink modules. So, to get the maximum benefit of this approach, we should only use the DRY coding principle in cases where the orange modules have low rates of change and high rates of reuse.

For example, business logic typically changes more often than base/platform logic (like reading an authentication token). A good example is using DRY with validation logic in the case of, say, managing domain objects provided by different services A, B and C, which different teams own. You manage service B and notice that all services use the same validation logic when reading and deleting objects. In this case, they all require object ids. So you decide to put this logic in a common validation module that services A, B and C are all updated to use.

Over time, this validation logic drifts. The business decides that to delete an object in service B, you must get approval from a team leader and that deletion must record their name for auditing purposes, changing the deletion validation logic to include their name. You decide to update the ‘common validation module’ but forget that services A and C should not have that behaviour. You update the common validation logic and deploy service B and everything seems fine because services A and C haven’t been changed and are still using the previous ‘ID only logic’. Next time, services A and C are changed and deployed two weeks later, their regression tests fail and their teams waste a lot of time tracking down the failure to a common module that was changed in the past by you. Yes, you can put validation of very generic things like email and numbers in a common validation module, but not validation of the entire request itself.

This is just a simple example, but other instances are much more nuanced and harder to spot, which requires a lot of experience. Moral of the story: don’t use DRY for parts of the code base that are very business-focused and likely to change often. Sometimes it is better to repeat yourself.

KISS – Use for code that exhibits high rates of change to keep costs down

Consider the following quote from Blaise PascaI: “If I had more time, I would have written a shorter letter”.

This is precisely the cost of KISS. It takes time to make things simple, elegant and a pleasure to use. You might ask, ‘Why don’t engineers just make good code straight away?’. There is some sense to this, but building software is an ‘exploratory process’. It is not like a production line where there is exactly one way to assemble a product but many different ways to assemble a product. Engineers often cannot see code in their minds and have to write it out to begin to make sense of it. Engineers will go through a few cycles of writing, testing and improving it – which we call ‘refactoring’. The more cycles they go through, the more they can simplify it.

So why don’t engineers just refactor more often? You need to think about the code you are writing to ensure it is worthy of KISS. In this case, ‘context is king’. If the code is throwaway, then it doesn’t make much sense to spend time on making it more elegant. Another way to think about it is in the categories of fast food code versus healthy food code.

Fast food code is bad for you, but it gives you instant gratification and meets your immediate needs. Then there is a healthy food code, which is good for you but takes more time to prepare. You can apply this approach to the whole code base or just to small parts of it. The key is to use KISS for parts of the code base that are likely to change often in the future. This is because it is harder to maintain and/or add new features to code that is messy and hard to reason about, leading to low productivity.

For example, you’ve been asked to create a new form to capture feedback for a one-off political campaign that won’t be repeated in the future. The link to the form will be passed on to the public who want to participate and the results will inform that party’s future policies. However, you spend far too much time building the form in such a way that it is easy for other engineers to work on or to add new features down the line. The party has a deadline to get this form out and it looks like you are not going to make it.

In this case, the code is unlikely to change much after the campaign is finished. It would have been better to write less elegant, less simple code overall as the code base is unlikely to see high rates of change in the future. You would still have to spend quality time in specific areas of the code base that carried the most risk, like any donations or personally identifiable information (PII), but you didn’t really need an elegant solution to logging, form submission, validation or content management

Adopting YAGNI, KISS, and DRY can streamline product development, reduce technical debt and keep your team focused on what truly matters – delivering value to customers. By applying these principles as sensible defaults, you’ll allow teams that flexibility to diverge when they need to, which will create more maintainable software, empower your team to move quickly and offer your users a simpler, more robust product experience. The minimum flexibility a team needs for these approaches are:

  • YAGNI – Use for code that has low technical inertia to mitigate significant delays
  • DRY – Use for code that has low rates of change and high rates of reuse to mitigate brittleness
  • KISS – Use for code that exhibits high rates of change to keep costs down

Remember, You Aren’t Gonna Need I,. Keep It Simple Stupid and Don’t Repeat Yourself. Use them as ‘sensible defaults’ to get the best mix of execution and innovation and watch your product and team flourish.

Davin Ryan

Davin Ryan

As Senior Coding Architect, Davin is passionate about delivering solutions that provide 'real' business value that wow our customers, knowledge sharing and applying DevOps principles.

More Ideas

our thoughts

Empowering developers with Containerised Development Workflows

Posted by Blair Jacobs . May 12.25

When HYPR works with a customer on their software systems, our mission is always to deliver the most value possible. In most organisations, we observe a range of challenges that developers face that make it difficult to provide value at pace.

> Read

our thoughts

How software companies stay relevant and responsive during organisational transformation

Posted by Daniel Walters . May 01.25

There often comes a time when business leaders look at how they are competing in the marketplace or how things are operating within and start to form a view that something needs to change. Even more often, when there is a new leader, they will form the view that something needs to change. These changes are usually structural and disruptive.

> Read

our thoughts

Lifting the lid: understanding what makes development teams thrive

Posted by Tony Luisi . Apr 21.25

Development teams are the engine of innovation in today’s digital business environment. While organisations increasingly demand more features from their development teams, few potentially understand what happens beneath the surface.

> Read

our thoughts

The AI coding partner: a generational shift in product engineering

Posted by Gareth Evans . Apr 14.25

We’ve recently witnessed a fundamental change in how software is engineered. Large Language Models (LLMs) are reshaping the landscape of product development in ways that extend beyond code completion and assistance. This shift isn’t just about productivity gains – it’s a new way of conceptualising, designing and building software products.

> Read

our thoughts

Flow metrics – build or buy?

Posted by Steven Gibson . Apr 08.25

It’s now well established that including flow metrics is an effective way to identify areas for improvement and visualise how work flows through your system.

> Read