Don’t Repeat Yourself (DRY) is one of the first programming principles that junior software developers learn. It is about keeping a shared piece of code in a single place (mostly method). Then it gets called from multiple clients, and whenever implementation changes, we need to change only the code in this one place. It looks appealing and makes perfect sense. When I started my career as a software developer, I strongly believed in the DRY principle. This started to change over time. In this blog, I have described different contexts of DRY in practice.
When does DRY make sense?
Keeping shared code in a single place makes sense in the scope of one module. We can extract some functionality used more than once into a method. This is often the case when people learn to program, where their applications are small in scale. The code can be tested quickly by manual run after a change. However, in the commercial project, multiple modules and developers are involved, and the situation completely changes.
Risk of DRY
Using the DRY principle correctly is difficult if there are no obvious boundaries between the modules. This can happen in a layered architecture of monolithic applications. For example, let us consider the sample project structure below.
In such a structure, it is possible to use a class in multiple modules. For instance, let us assume that in OrderController, we need a functionality that already exists in CustomerRepository, and we use it for the OrderController.
In theory, it is easy to follow the DRY rule. However, if the boundaries between modules are violated and are not separated anymore, it gets tough when we go for testing. After applying a change in one place (that is shared to follow DRY), we need to test all places when it is used. If we do not do this, we risk occurrences of unexpected errors. Such systems must have robust regression testing (often manual and time-consuming) to ensure that all functionalities work.
Reusing code gets more challenging when the team grows. In software projects, there is often a library called Common used in multiple applications. It is absolutely fine for infrastructure code like security, logging, etc. However, if a new LINQ extension method is added to a Common library, it is not guaranteed that other developers will remember and will be able to find it quickly. In my current project, we often face a problem that breaking change is introduced in the Common library, and the client application (that was not tested by the developer) breaks. Keeping such a library in good shape is a challenge when the DRY principle is used.
Architecture to the rescue
The design with poorly separated modules is called a big ball of mud. What can we do to avoid ending up with such a system? Architectural styles such as modular monolith, microservices, CQRS, DDD, or vertical slice architectures protect themselves from overusing the DRY rule by keeping strict boundaries between modules. If you want to read more about maintaining the components isolated, visit my blog post here. It is obvious that we do not want to use the same views, business logic, and data access code in different microservices (it is often not possible). This is especially useful in big development teams when it is challenging to review every code change thoroughly.
Source control to the rescue
If there are good reasons not to use DRY, then possibly multiple modules require code change to support a new feature. For instance, introducing new property that must be passed through all layers. In this case, we can use source control (or project documentation) to store the change details, so it is easier to apply similar changes in the future.
When can we apply the DRY principle?
The answer is not straightforward and depends on the context. However, here are some general hints:
- Use in small components (method, class, module).
- Do not reuse business logic in multiple modules.
- Consider testing. If testing the change becomes more complex, it is better to avoid using DRY.
- Consider the team size; ideally, utility code is shared across one development team. It can be shared across multiple teams when it benefits the project but remember to send out proper communication (and maybe versioning) about changes made. Also, ensure to add unit tests to the shared code to avoid regression errors.
There is no denying that the DRY principle is a must-have for good code quality. However, it can be challenging at times and result in a system that is hard to maintain. The decision of whether to use DRY is not simple; however, it is important for developers to be aware of the consequences.