I was going to add this as a response to a recent Tailwind thread, but it’s something I see come up time and time again: people misunderstanding the DRY principle. I feel like this is something you quickly learn with experience, but I want to get the discussion out there in the open.
The DRY principle is one of the most misunderstood principles in software engineering. Just because two repetitions or code look the same does not mean they are the same. DRY is more often than not misapplied. DRY is about having a consistent source of truth for business logic more than anything. It’s about centralizing knowledge, not eliminating repetition. Every time you write reusable code, you’re creating a coupling point between parts of the system. You should be careful in doing so.
There is a…
I was going to add this as a response to a recent Tailwind thread, but it’s something I see come up time and time again: people misunderstanding the DRY principle. I feel like this is something you quickly learn with experience, but I want to get the discussion out there in the open.
The DRY principle is one of the most misunderstood principles in software engineering. Just because two repetitions or code look the same does not mean they are the same. DRY is more often than not misapplied. DRY is about having a consistent source of truth for business logic more than anything. It’s about centralizing knowledge, not eliminating repetition. Every time you write reusable code, you’re creating a coupling point between parts of the system. You should be careful in doing so.
There is a real cost to premature abstraction. Say you have two buttons that both use bg-blue-500 text-white px-4 py-2 rounded. A DRY purist would immediately extract this into a .btn-primary class or component. But what happens when the designer asks you to make one button slightly larger, or change the color of just one? Now you’re either breaking the abstraction or creating .btn-primary-large variants. You’ve traded simple, explicit code for a brittle abstraction.
The same thing happens with JavaScript. You see two functions that share a few lines and immediately extract a utility. But when requirements change, you end up with utility functions that take a dozen parameters or do completely different things based on flags. The cure becomes worse than the disease.
Coupling is the real enemy. Every time you create a reusable piece of code, you’re saying “these things will always change together.” But how often is that actually true? When you abstract too early, you’re making a bet that two separate parts of your system will evolve in lockstep. Most of the time, you lose that bet. This coupling makes refactoring a nightmare. Want to change how one part works? First you need to understand how it affects every other part that shares the abstraction. Want to delete a feature? Good luck untangling it from the shared utilities it depends on.
The obsession with eliminating visual repetition often leads to premature abstraction. Sometimes repetition is actually good. It makes code more explicit, easier to understand, and prevents tight coupling between unrelated parts of your system.
When people complain that Tailwind violates DRY, they’re missing the point entirely. CSS classes aren’t business logic. Having flex items-center in multiple places isn’t violating DRY any more than using the same variable name in different functions.
When does DRY actually matter? DRY has its place. You absolutely should centralize business logic, validation rules, and data transformations. If your user authentication logic is duplicated across your codebase, that’s a real DRY violation. If your pricing calculation algorithm exists in multiple places, fix that immediately.
The key is distinguishing between knowledge and coincidence. Two pieces of code that happen to look similar today might evolve completely differently tomorrow. But business rules? Those should have a single source of truth.
There’s a better approach. Start with repetition. Make it work first, then identify patterns that actually represent shared knowledge. And if you think an abstraction should exist, you can always formalize it later by creating a reusable component, function, or shared service.
You can always extract an abstraction later, but you can rarely put the toothpaste back in the tube.