Skip to main content

DRY (don't repeat yourself)

DRY (Don't Repeat Yourself) - every piece of logic or knowledge in a system has exactly one place where it lives.

Theory

TL;DR

  • Analogy: one master key that opens all doors vs. copying keys everywhere. One copy breaks and security fails.
  • Main benefit: change logic once, and everything that depends on it updates automatically.
  • WET (Write Everything Twice) forces updates in multiple spots, which leads to inconsistencies and bugs from mismatched copies.
  • Decision rule: if you copy-pasted code or data, extract it to a function, constant, or module.

Quick example

javascript
// WET: name format exists in two places // Fix one, forget the other = silent bug function userFullName(first, last) { return first + ' ' + last; } function userGreeting(first, last) { return 'Hi, ' + first + ' ' + last + '!'; } // DRY: greeting reuses the single name logic function userFullName(first, last) { return first + ' ' + last; } function userGreeting(first, last) { return 'Hi, ' + userFullName(first, last) + '!'; } // userGreeting('Alice', 'Smith') → 'Hi, Alice Smith!'

The WET version has name formatting in two places. Fix a bug in one and forget the other. The DRY version has one place to change.

WET vs DRY

Duplicated code forces updates in multiple spots at once. Miss one and you get inconsistency, like a password validation rule checking 8 characters in a form but 6 in an API handler. DRY centralizes the logic: one function, one constant, one module. Change it there and everything that depends on it updates automatically.

The opposite of DRY is sometimes called WET (Write Everything Twice). That name is only half-joking.

When to extract

  • Copy-pasting similar logic across functions or files? Extract to a shared function.
  • Same string or number appearing in multiple files? Move it to a named constant.
  • Identical validation in a form, an API handler, and a test? Put it in a shared utility.
  • Repeated API boilerplate across routes? Wrap in a factory function.
  • Config values scattered across files? Centralize them in one config object.

Runtime behavior

Compilers like V8 or TypeScript execute each copy of duplicated logic independently. A bug in duplicated code runs in every copy separately. Some ESLint rules like no-dupe-keys flag specific patterns statically, but most duplication only surfaces at runtime when copies get out of sync.

Common mistakes

Mistake: over-DRYing trivial code.

javascript
// Pointless abstraction - adds cognitive load, saves nothing const twiceTwo = multiply(2, 2);

Abstract only when logic is reused 3+ times or is genuinely complex. Two functions that look similar today may diverge tomorrow. Forcing them into one abstraction early makes both harder to change later.

Mistake: duplicating for "readability."

javascript
// WET: adding a new role means updating three spots if (admin || moderator || owner) { ... } // DRY: one place to update const canEdit = ['admin', 'moderator', 'owner'].includes(role);

Permission bugs in production often trace back to someone adding a role in one if block but not the other two.

Mistake: forgetting data duplication.

javascript
// Wrong: API version hardcoded in 5 separate files { name: 'API v1', version: '1.0' } // Fix export const API_VERSION = '1.0';

DRY applies to data, not just logic. A version bump should touch one file.

Mistake: two functions with the same name but different logic in different modules.

javascript
// utils.js has validateEmail() // components/validateEmail.js has a different validateEmail()

Import the wrong one and you get failures with no obvious cause. One module, barrel exports, done.

Real-world usage

  • React: shared useAuth hook across pages instead of repeating auth logic in every component (NextAuth.js pattern).
  • Express: middleware factory requireRole('admin') instead of inline role checks on each route (Passport.js pattern).
  • Node.js: path.join(__dirname, 'config') defined once and imported wherever needed.
  • Redux: centralized action creators so payload shape lives in one file.
  • TypeScript: shared User interface across NestJS models instead of redefining per file.

One thing teams get wrong consistently: they DRY the code but leave the database URL hardcoded in 10 scripts. Data duplication is just as expensive as code duplication.

Follow-up questions

Q: What is the opposite of DRY?
A: WET, which stands for Write Everything Twice (or sometimes "We Haven't Estimated Anything Totally"). It describes code where the same logic lives in multiple places.

Q: When does DRY hurt more than it helps?
A: When you abstract too early. Use the rule of three: wait until you have at least three duplications before extracting. Two similar functions may diverge tomorrow, and a forced abstraction makes both harder to change.

Q: How does DRY relate to SOLID?
A: DRY supports the Single Responsibility Principle. When logic lives in one place, that place is responsible for it and nothing else.

Q: In microservices, how do you apply DRY across services without coupling them?
A: Share pure utility packages (validation schemas, date formatters) freely. Avoid sharing domain logic across bounded contexts because that creates coupling. For domain events, use contracts or event schemas rather than shared code. The senior answer here mentions Domain-Driven Design and acknowledges that duplication across services is sometimes acceptable to keep services independent.

Examples

Express.js: shared query fetcher

javascript
// WET: the same query pattern repeats in every route app.get('/users', authMiddleware, (req, res) => { db.query('SELECT * FROM users WHERE active=1').then(users => res.json(users)); }); app.get('/orders', authMiddleware, (req, res) => { db.query('SELECT * FROM orders WHERE active=1').then(orders => res.json(orders)); }); // DRY: one function owns the pattern const fetchActive = (table) => db.query(`SELECT * FROM ${table} WHERE active=1`); app.get('/users', authMiddleware, (req, res) => fetchActive('users').then(res.json.bind(res))); app.get('/orders', authMiddleware, (req, res) => fetchActive('orders').then(res.json.bind(res)));

Adding a new route is one line, not a copy of the query string. The WHERE active=1 condition lives once. Change it to WHERE status='active' and every route updates.

React: custom hook for shared fetch logic

javascript
// WET: fetch logic copy-pasted across components function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []); } // DRY: extracted to a reusable hook function useApiData(url) { const [data, setData] = useState([]); useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url]); // url in deps - miss this and you get stale data on every url change return data; } const users = useApiData('/api/users'); const products = useApiData('/api/products');

The dependency array with url is where developers slip up. Miss it and the hook fetches the initial URL forever, even if url changes. One hook implemented correctly once is safer than five fetch blocks scattered across components.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?