In my journey as a Software Developer, I’ve often seen this same pattern, I start a small project, identify a piece of logic used in two places, and immediately shout, "Don’t Repeat Yourself (DRY)!". I extract that logic into a "shared" or "common" folder, and for a few weeks, everything is perfect. But then, the project grows. Requirement A changes for Service 1, but Service 2 needs the old behavior. Suddenly, the "reusable" component is full of if/else blocks and "hacky" flags or arguments. I haven’t saved time, I created a highly coupled bottleneck. Building clean, maintainable codebases isn't just about avoiding repetition, it's about knowing when to let code evolve from being Reusable to being Decoupled.
The Problem: When "DRY" Becomes a Liability
A perfect example would be working on modular UIs (componentization) where the goal is high component reusability. However, one can quickly learn that there is a "Cost of Extraction". Also from a DevOps perspective, code coupling is a deployment nightmare. For instance, if Service A and Service B share a "reusable" library that is tightly coupled.
- Premature Abstraction: If you dry up code too early (before you truly understand the domain), you force two unrelated features to share the same DNA.
- The Shared Folder of Doom: We’ve all seen the utils or common folder that grows into a 5,000-line monster. This is often where "reusable" code goes to die, making it impossible to update one part of the app without risking a regression in another.
- Failed Gates: A minor UI tweak in Service A can trigger a failure in the SonarQube quality gate or Playwright E2E tests for Service B.
- Monolithic Deployment: You lose the ability to deploy features independently. You end up having to test and deploy the entire application suite just to update a single button.
Knowing When to Evolve to "Decoupled"
As an engineer who thrives at the intersection of development and automation, I use these three "signals" to decide when to move from DRY to Decoupled.
Signal A: The "Flag" Smell
If you find yourself adding parameters like isForHeroSection or hideInFooter to a shared function or component, the code is no longer truly reusable. It’s now a conditional mess.
- The Move: Duplicate the logic. Yes, you heard me. Copy-paste the code into the specific feature folder and let it evolve independently.
Signal B: Divergent Change
If every time you change a feature, you find yourself having to edit four different files in a "shared" directory, your boundaries are wrong.
- The Move: Apply Domain-Driven Design (DDD) principles. Group code by "Feature" (e.g., Auth, Payments, Streaming) rather than "Type" (e.g., Components, Services).
Signal C: Scaling Performance
In huge enterprise level projects, tight coupling often leads to performance bottlenecks. If two NestJs modules share a database schema but have different access patterns, they should be decoupled to allow for optimized indexing on a per-module basis.
Practical Implementation: The "Feature-First" Folder Structure
Instead of a giant shared/components folder, try a Feature-Sliced approach, e.g:
- features/request-management/* (Contains its own logic, UI, and API calls).
- features/sales-tracking/* (Contains its own logic, UI, and API calls).
- shared/ui/* (Only for purely visual, "dumb" components like Buttons or Inputs).
Final Thought: Optimize for Change, Not Just Conciseness
Our goal is to build maintainable systems. Sometimes, maintainability means writing more code (decoupling) so that in six months, you can delete or change a feature in seconds without a production outage. Everyone involved in the SDLC should have this in mind. For instance, a designer will not give the developer a component which makes absolute sense to abstract and reuse and make very subtle nuances in the different places where this component is used. If the designer is well informed, he's expected to start the abstraction in his work and will soon realise that making small nuances like that make it had to reuse code and keep it maintainable.
Remember you don't only need DRY, also consider KISS.
Top comments (2)
I wonder if composability would help in this instance? It might allow you to keep things DRY, and still evolve independently.
Keep the rendering part doing it's one render job, and make a new "controller" that includes that bit, along with the conditional that decides either to display it or an empty string for example "in the footer"
It seems like some of the DRY with flags kinds of issues are more of a Single Responsibility issue sometimes.
Thank you for sharing this article!
I'll keep these in mind.