1786194754

Stop Over-DRYing Your Code: How Premature Abstractions Kill Codebases


You open the project repository to make a small change on a checkout screen. Based on the ticket, it looked like a ten-minute job. But when you find the function responsible for the flow, you realize it has over three hundred lines filled with nested ifs, recursive calls, and conditional flags like isMobile or skipTaxCalculation. Investigating the Git history a bit, you discover that this function was created two years ago just to avoid repeating about five lines in another corner of the system. Today it serves four completely different flows, and trying to touch a simple checkout rule breaks the financial reporting module in production. It is the exact kind of situation where the blind pursuit of never repeating code ends up creating a monster. In college and in internet videos, the DRY principle is usually taught as a sacred rule that should never be broken. But in the day-to-day reality of programming, the uncritical use of this rule is responsible for some of the worst codebases I have ever seen. The truth is that we need to ask to what extent keeping things strictly without duplication is actually healthy for the project. The main problem here is not the concept itself, but how we interpret it. When Andy Hunt and Dave Thomas wrote about this in The Pragmatic Programmer, the core idea was that every piece of knowledge in the system should have a single representation. Notice that they were talking about knowledge and business rules, not identical lines of text in the editor. If two different screens have similar snippets of code, like three lines formatting a date, but belong to business contexts with no relationship, that is not duplication of knowledge. It is just a syntactic coincidence. When you force a generic abstraction to join those two corners, you are not cleaning up the system. You are just coupling things that should evolve separately. Obviously, this does not mean we should go around duplicating everything in sight without thinking. DRY remains important when applied in the right spots. Critical business rules, for instance, such as tax calculations or document validations, do need to stay centralized. If legislation changes a tax rate, you want to alter that in a single place without hunting through files across the project. The same goes for infrastructure things, API clients, or base components of the company's design system. DRY works well when it protects the code against serious logical inconsistencies. The problem is when the remedy turns into poison and the team overdoes it. One of the classic symptoms of this is the emergence of functions packed with boolean arguments that serve only to handle exceptions for each call. Slowly, the cognitive load to understand a simple snippet becomes huge because you have to keep jumping from file to file across multiple levels of abstraction. A point comes where nobody wants to touch that utility function anymore for fear of breaking a part of the system that shouldn't even be related to what is being changed. You can see this clearly in practice. Imagine someone created a single function to process data for standard users and administrators just to save a few lines: ```typescript function processUserData(user: User, isAdmin: boolean, sendNotification: boolean) { const formattedName = user.name.trim().toUpperCase(); if (isAdmin) { auditLog.register(user.id); } if (sendNotification) { if (isAdmin) { emailService.sendAdminAlert(user.email); } else { emailService.sendWelcome(user.email); } } return { id: user.id, name: formattedName, role: isAdmin ? 'ADMIN' : 'USER' }; } ``` The code tries to embrace all scenarios in the same place and ends up rigid. If we accept repeating a trivial detail, like name formatting, we can separate things in a much simpler way: ```typescript function processStandardUser(user: User) { const formattedName = user.name.trim().toUpperCase(); emailService.sendWelcome(user.email); return { id: user.id, name: formattedName, role: 'USER' }; } function processAdminUser(user: User) { const formattedName = user.name.trim().toUpperCase(); auditLog.register(user.id); emailService.sendAdminAlert(user.email); return { id: user.id, name: formattedName, role: 'ADMIN' }; } ``` If the admin flow changes completely next week, the code for standard users keeps running without being affected. To avoid creating abstractions prematurely, it helps to keep a few ideas in mind. Sandi Metz has a great quote about this where she says that it is much cheaper to deal with duplication than with the wrong abstraction. When in doubt, prefer clarity over trying to write the fewest possible lines. There is also that old rule of tolerating the first and second duplication, saving abstraction for when the same pattern appears for the third time. Writing the same thing twice is usually not the end of the world. When reviewing someone's code or deciding whether it is worth creating a generic function, you can try analyzing the reason for change in that snippet. If the rule changes on screen A, does it mandatorily need to change on screen B? If the answer is no, it is best to keep them separate. It is also worth evaluating whether the abstraction is truly making the logic simpler or just hiding complexity behind a pile of ifs. Good code is not the kind that is super compact or full of syntax tricks; it is what your team can understand and change six months from now without getting frustrated. Accepting occasional duplication is usually a very small price to pay to keep the system decoupled and readable. Next time you catch five similar lines in the code, maybe it is worth waiting a bit before pulling that into a global utility function.

(0) Comments

Welcome to Chat-to.dev, a space for both novice and experienced programmers to chat about programming and share code in their posts.

About | Privacy | Donate
[2026 © Chat-to.dev]