Skip to content

Rule of Three - Refactoring Guideline

The Rule of Three is an empirical guideline in code refactoring. It helps developers decide when to refactor similar code snippets to avoid duplication. The rule states:

When you have two instances of similar code, it’s usually okay to leave them as-is. But when you have three instances, it’s time to refactor and extract the common logic into a new procedure.

This rule was popularized by Martin Fowler in his book Refactoring: Improving the Design of Existing Code and is attributed to Don Roberts.


  • Maintenance Overhead: When the same logic is duplicated in multiple places, any change to that logic must be applied consistently across all instances. This increases the risk of errors and inconsistencies.
  • Readability: Duplicated code makes the codebase harder to read and understand.
  • Scalability: As the codebase grows, duplicated logic becomes increasingly difficult to manage.
  • Wrong Abstraction: Refactoring too early (e.g., after only two instances) can lead to overly generic or incorrect abstractions. This can make the code harder to understand and maintain as requirements evolve.
  • Over-Engineering: Introducing unnecessary complexity for hypothetical future needs.

Two Instances

It’s usually fine to leave two similar code snippets as-is. The cost of refactoring may outweigh the benefits.

Three Instances

When you encounter a third instance of similar code, it’s time to refactor. Extract the common logic into a reusable function or method.

Beyond Three

If you already have a reusable procedure, adding more instances is straightforward and maintains consistency.


  1. Identify Duplication Scan your codebase for similar logic or patterns repeated in multiple places.
  2. Evaluate the Cost Assess whether the duplication is causing maintenance issues or if it’s likely to grow.
  3. Extract Common Logic If you find three or more instances, extract the common logic into a new function or method.
  4. Replace Duplicates Replace each duplicated instance with a call to the new function or method.
  5. Test Thoroughly Ensure the refactored code works as expected and doesn’t introduce new bugs.
  6. Document the Change Add comments or documentation to explain the purpose of the new function or method.

Suppose you have the following duplicated logging logic in multiple places:

// File 1
console.log(`[INFO] ${new Date().toISOString()}: User ${userId} logged in.`);
// File 2
console.log(`[INFO] ${new Date().toISOString()}: User ${userId} logged out.`);
// File 3
console.log(`[INFO] ${new Date().toISOString()}: User ${userId} updated profile.`);
utils/logger.js
function logInfo(message, userId) {
console.log(`[INFO] ${new Date().toISOString()}: User ${userId} ${message}.`);
}
// File 1
logInfo("logged in", userId);
// File 2
logInfo("logged out", userId);
// File 3
logInfo("updated profile", userId);

Suppose you have the following duplicated discount calculation logic:

# File 1
def calculate_discount(price, discount_rate):
return price * (1 - discount_rate)
# File 2
def apply_discount(price, discount_rate):
return price * (1 - discount_rate)
# File 3
def get_final_price(price, discount_rate):
return price * (1 - discount_rate)
utils/discount.py
def calculate_discount(price, discount_rate):
"""Calculate the final price after applying a discount."""
return price * (1 - discount_rate)
# File 1, 2, and 3
from utils.discount import calculate_discount

Suppose you have the following duplicated input validation logic:

// File 1
public boolean isValidEmail(String email) {
return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
}
// File 2
public boolean checkEmail(String email) {
return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
}
// File 3
public boolean validateEmail(String email) {
return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
}
utils/ValidationUtils.java
public class ValidationUtils {
public static boolean isValidEmail(String email) {
return email != null && email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
}
}
// File 1, 2, and 3
import utils.ValidationUtils;
ValidationUtils.isValidEmail(email);


Start Small

Begin by refactoring small, manageable chunks of code. This reduces the risk of introducing bugs.

Test After Refactoring

Always test your code after refactoring to ensure the changes work as expected.

Document Your Changes

Add comments or documentation to explain the purpose of the new function or method.

Use Meaningful Names

Name your extracted functions or methods clearly to reflect their purpose.


Real-World Example: Refactoring a Web Application

Section titled “Real-World Example: Refactoring a Web Application”

Suppose you’re working on a web application with multiple endpoints that validate user input. Initially, the validation logic is duplicated across several files:

routes/user.js
app.post('/register', (req, res) => {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).send('Invalid email');
}
// ...
});
// routes/profile.js
app.put('/update', (req, res) => {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).send('Invalid email');
}
// ...
});
// routes/auth.js
app.post('/login', (req, res) => {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).send('Invalid email');
}
// ...
});
utils/validation.js
function validateEmail(email) {
if (!email || !email.includes('@')) {
throw new Error('Invalid email');
}
}
// routes/user.js
app.post('/register', (req, res) => {
try {
validateEmail(req.body.email);
// ...
} catch (error) {
return res.status(400).send(error.message);
}
});
// routes/profile.js
app.put('/update', (req, res) => {
try {
validateEmail(req.body.email);
// ...
} catch (error) {
return res.status(400).send(error.message);
}
});
// routes/auth.js
app.post('/login', (req, res) => {
try {
validateEmail(req.body.email);
// ...
} catch (error) {
return res.status(400).send(error.message);
}
});



DRY (Don't Repeat Yourself)

A principle of software development aimed at reducing repetition of software patterns.

Code Smells

Indicators of potential problems in the codebase, such as duplicated code, long methods, or large classes.

SOLID Principles

A set of five design principles to make software designs more understandable, flexible, and maintainable.