1. Introduction
Duplicate code appears when the same or nearly the same logic is implemented in multiple places instead of being defined once and reused appropriately.
In real Java applications, duplication commonly appears in:
- Service classes
- REST controllers
- Validation logic
- Entity-to-DTO mapping
- Exception handling
- Repository query preparation
- Logging
- External API integration
- Business-rule calculations
- Notification logic
- Date and currency formatting
- Authentication and authorization checks
A small amount of duplication may initially look harmless.
For example, two service methods may independently calculate an order discount:
public BigDecimal calculateOnlineOrderDiscount(Order order) {
if (order.getTotalAmount().compareTo(new BigDecimal("5000")) >= 0) {
return order.getTotalAmount().multiply(new BigDecimal("0.10"));
}
return BigDecimal.ZERO;
}
public BigDecimal calculateStoreOrderDiscount(Order order) {
if (order.getTotalAmount().compareTo(new BigDecimal("5000")) >= 0) {
return order.getTotalAmount().multiply(new BigDecimal("0.10"));
}
return BigDecimal.ZERO;
}Both methods currently behave correctly.
The problem appears when the business changes the minimum amount from 5000 to 6000.
If one method is updated and the other is forgotten, the application now contains inconsistent business behavior.
This is why duplicate code is not merely a cosmetic problem.
It can create:
- Inconsistent business rules
- Repeated bugs
- More expensive maintenance
- Larger Pull Requests
- Harder testing
- Difficult production debugging
- Higher regression risk
Avoiding duplicate code means identifying knowledge or behavior that should have a single authoritative implementation and placing it in the correct reusable abstraction.
The goal is not to eliminate every repeated line.
The goal is to eliminate duplication that represents the same responsibility or business knowledge.
2. What This Topic Means
In Java development, avoiding duplicate code means ensuring that the same business rule, algorithm, transformation, or technical behavior is not unnecessarily implemented in multiple locations.
Consider:
public boolean isPremiumCustomer(Customer customer) {
return customer.getTotalOrders() >= 20
&& customer.getTotalSpent().compareTo(new BigDecimal("100000")) >= 0;
}If the same condition appears independently in:
DiscountServiceRewardServiceShippingServiceCustomerController
then the system has four separate implementations of the same business concept.
That is duplication of knowledge, not just duplication of text.
A better design might provide one authoritative implementation:
public boolean isPremiumCustomer(Customer customer) {
return customer.getTotalOrders() >= PREMIUM_ORDER_COUNT
&& customer.getTotalSpent().compareTo(PREMIUM_SPENDING_THRESHOLD) >= 0;
}Other components call that rule instead of redefining it.
During code review, developers should distinguish between:
Accidental duplication
Two developers independently implement the same logic.
Copy-paste duplication
Existing code is copied into another method and slightly modified.
Business-rule duplication
The same domain decision appears in several services.
Structural duplication
Multiple classes have identical workflow steps.
Validation duplication
The same input validation is repeated across controllers or services.
Mapping duplication
The same entity-to-DTO conversion is implemented repeatedly.
Error-handling duplication
The same try-catch, logging, and exception translation appears in multiple methods.
Not every repeated statement needs abstraction.
A good reviewer asks:
Does this duplication represent the same concept that should change for the same reason?
If yes, it is a strong candidate for consolidation.
3. Why It Matters in Real Projects
Readability
Duplicate code makes developers inspect multiple implementations to understand the actual system behavior.
Suppose discount eligibility exists in three services.
A developer investigating the rule must determine:
- Are all three implementations identical?
- Which implementation is current?
- Is one legacy?
- Which one should be modified?
A single authoritative implementation makes the system easier to understand.
Maintainability
This is the biggest cost of duplication.
If a business rule exists in five locations, every change may require five modifications.
Missing even one location creates inconsistent behavior.
For example:
amount.compareTo(new BigDecimal("10000")) >= 0may represent a free-shipping threshold.
If duplicated across four services, changing the threshold becomes risky.
Debugging
Duplicate implementations make production bugs harder to diagnose.
One API may return the correct value while another API returns a different result because only one copy was fixed.
Developers then need to locate all duplicated implementations before understanding the failure.
Reliability
Bug fixes must be applied consistently.
If the same defective calculation appears in multiple locations, fixing one copy does not fix the system.
Duplicate code therefore increases regression risk.
Performance
Duplication itself does not automatically cause runtime performance problems.
However, duplicated code may also duplicate expensive operations.
For example, several components may independently execute the same repository query or external API call because shared information was not centralized.
Performance should be evaluated based on what the duplicated implementation actually does.
Scalability of Development
As the codebase and engineering team grow, duplication becomes increasingly expensive.
With dozens of developers working across microservices and modules, duplicated business logic can quickly diverge.
Team Development
Duplicate logic causes uncertainty during code review:
- Which implementation is authoritative?
- Should both be changed?
- Are they intentionally different?
- Is the copied logic still valid?
Clear ownership of shared business behavior reduces this ambiguity.
4. Core Concept
The central idea behind avoiding duplicate code is often described by the DRY principle:
Don't Repeat Yourself.
However, DRY should not be interpreted as:
Never write the same line twice.
A more useful interpretation is:
A piece of business knowledge should have one authoritative representation.
Consider these two methods:
public boolean isValidEmail(String email) {
return email != null && email.contains("@");
}
public boolean isValidUsername(String username) {
return username != null && username.length() >= 5;
}Both contain a null check.
That does not automatically mean the null check must be extracted.
The two methods represent different rules.
Now consider:
public boolean canApplyDiscount(Customer customer) {
return customer.getLoyaltyPoints() >= 1000;
}
public boolean canReceiveReward(Customer customer) {
return customer.getLoyaltyPoints() >= 1000;
}If both rules mean exactly the same business concept—such as loyalty eligibility—then the duplicated knowledge should probably be centralized.
Same Text Does Not Always Mean Same Responsibility
These methods contain similar code:
order.setUpdatedAt(Instant.now());
customer.setUpdatedAt(Instant.now());That repetition may be perfectly acceptable because two separate objects independently require timestamps.
Different Text Can Still Represent Duplicate Knowledge
Consider:
return customer.getAge() >= 18;and:
if (user.getDateOfBirth().plusYears(18).isBefore(LocalDate.now())) {
...
}Although the code looks different, both may implement the same legal-age rule.
That is semantic duplication.
The Correct Abstraction Matters
Duplicate code can be removed using:
- Private helper methods
- Domain methods
- Shared service methods
- Mapper components
- Validator classes
- Utility classes
- Base classes where inheritance genuinely represents the design
- Composition
- Reusable Spring components
- Repository methods
- Configuration
- Strategy implementations when behavior actually varies
The simplest appropriate abstraction should be preferred.
5. Important Rules
- Remove duplicated business knowledge, not every repeated statement.
- Extract logic only when the duplicated code represents the same responsibility.
- Prefer meaningful domain methods over generic utility methods.
- Keep one authoritative implementation of important business rules.
- Do not copy-paste code and modify only one small condition.
- Consolidate repeated validation when it genuinely applies to multiple flows.
- Reuse repository methods instead of recreating equivalent queries.
- Centralize repeated mapping logic when several classes convert the same models.
- Do not create a large
CommonUtilsclass containing unrelated behavior. - Prefer composition over inheritance when reuse is the only reason for inheritance.
- Avoid premature abstraction when two implementations only look similar.
- Wait until the common concept is understood before designing a reusable abstraction.
- Ensure extracted shared methods have clear ownership.
- Keep reusable code focused and cohesive.
- Add tests around shared business logic because changes may affect multiple callers.
- Review whether duplicated code performs repeated database or network operations.
- Avoid global abstractions that create unnecessary coupling between unrelated modules.
6. Bad Code Example
Consider a Spring Boot order-processing application.
Two methods process different order types.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final EmailService emailService;
public OrderService(OrderRepository orderRepository, EmailService emailService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
}
public Order processOnlineOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID is required");
}
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
BigDecimal total = order.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setTotalAmount(total);
if (total.compareTo(new BigDecimal("5000")) >= 0) {
order.setShippingCharge(BigDecimal.ZERO);
} else {
order.setShippingCharge(new BigDecimal("200"));
}
order.setStatus(OrderStatus.CONFIRMED);
Order savedOrder = orderRepository.save(order);
emailService.sendOrderConfirmation(savedOrder);
return savedOrder;
}
public Order processPhoneOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID is required");
}
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
BigDecimal total = order.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setTotalAmount(total);
if (total.compareTo(new BigDecimal("5000")) >= 0) {
order.setShippingCharge(BigDecimal.ZERO);
} else {
order.setShippingCharge(new BigDecimal("200"));
}
order.setStatus(OrderStatus.CONFIRMED);
Order savedOrder = orderRepository.save(order);
emailService.sendOrderConfirmation(savedOrder);
return savedOrder;
}
}The two methods are almost identical.
The only conceptual difference is the order source.
7. Problems in the Bad Code
Code Smell: Copy-Paste Duplication
processOnlineOrder and processPhoneOrder contain almost identical implementations.
Any future modification must be applied twice.
Duplicated Validation
These checks appear in both methods:
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID is required");
}if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}This validation represents the same business requirement.
Duplicated Calculation Logic
The order total calculation is repeated.
If tax, discount, or rounding behavior changes, both copies need modification.
Duplicated Shipping Rule
The free-shipping rule appears twice:
if (total.compareTo(new BigDecimal("5000")) >= 0) {
order.setShippingCharge(BigDecimal.ZERO);
} else {
order.setShippingCharge(new BigDecimal("200"));
}This is important business knowledge.
Duplicating it creates a high risk of inconsistent behavior.
Magic Values
The values:
5000200
represent business rules but are hardcoded.
Duplicated Workflow
Both methods:
- Validate the order
- Calculate total
- Calculate shipping
- Confirm the order
- Save the order
- Send confirmation email
The workflow itself is duplicated.
Regression Risk
Suppose the business changes free shipping from 5000 to 6000.
A developer may update:
processOnlineOrder()but forget:
processPhoneOrder()Online and phone orders now behave differently.
Testing Duplication
Tests are likely to repeat the same scenarios for both methods.
That increases test maintenance.
Larger Pull Requests
Every business-rule modification requires multiple code changes, making PRs larger and harder to review.
8. Code Review Findings
A senior Java developer reviewing this implementation should identify:
- The two methods contain nearly identical workflows.
- Order validation is duplicated.
- Total calculation is duplicated.
- Shipping-charge calculation is duplicated.
- Business thresholds are hardcoded in multiple locations.
- Saving and confirmation-email behavior is repeated.
- The methods are likely to diverge during future maintenance.
- The real variation—order source—is not represented explicitly.
- A shared processing method can remove the duplication without introducing a complex design.
- There is no reason to introduce inheritance or a design pattern yet because a simple method extraction is sufficient.
The reviewer should also verify whether online and phone orders are intentionally identical.
Removing duplication without confirming that assumption could accidentally merge two business processes that may evolve differently.
9. Reviewer Comment Example
processOnlineOrderandprocessPhoneOrdercurrently duplicate almost the entire workflow. Could we extract the common order-processing steps into one method and keep only source-specific behavior separate?
The free-shipping threshold is duplicated business logic. Please move it to one authoritative method or component so future rule changes cannot diverge between order channels.
The validation block is identical in both methods. Can we centralize it in a focused
validateOrdermethod?
Please extract the shipping threshold and standard shipping charge into named constants or configuration properties.
Before merging the flows, can we confirm that online and phone orders intentionally use the same validation, pricing, and confirmation rules?
I would avoid introducing inheritance here. A small shared processing method appears sufficient for the current requirements.
10. Improved Code
@Service
public class OrderService {
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("5000.00");
private static final BigDecimal STANDARD_SHIPPING_CHARGE = new BigDecimal("200.00");
private final OrderRepository orderRepository;
private final EmailService emailService;
public OrderService(OrderRepository orderRepository, EmailService emailService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
}
public Order processOnlineOrder(Order order) {
return processOrder(order, OrderSource.ONLINE);
}
public Order processPhoneOrder(Order order) {
return processOrder(order, OrderSource.PHONE);
}
private Order processOrder(Order order, OrderSource source) {
validateOrder(order);
order.setSource(source);
BigDecimal total = calculateOrderTotal(order);
order.setTotalAmount(total);
order.setShippingCharge(calculateShippingCharge(total));
order.setStatus(OrderStatus.CONFIRMED);
Order savedOrder = orderRepository.save(order);
emailService.sendOrderConfirmation(savedOrder);
return savedOrder;
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getCustomerId() == null) {
throw new IllegalArgumentException("Customer ID is required");
}
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
}
private BigDecimal calculateOrderTotal(Order order) {
return order.getItems()
.stream()
.map(this::calculateItemTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private BigDecimal calculateItemTotal(OrderItem item) {
return item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()));
}
private BigDecimal calculateShippingCharge(BigDecimal total) {
if (total.compareTo(FREE_SHIPPING_THRESHOLD) >= 0) {
return BigDecimal.ZERO;
}
return STANDARD_SHIPPING_CHARGE;
}
}Possible enum:
public enum OrderSource {
ONLINE,
PHONE
}11. Improved Code Explanation
Shared Workflow
Both entry points now delegate to:
processOrder(order, source)The order-processing workflow exists in only one place.
Source-Specific Information Remains Explicit
The methods:
processOnlineOrder()and:
processPhoneOrder()still communicate their intent clearly.
They are not removed unnecessarily.
Validation Is Centralized
The repeated validation rules now exist in:
validateOrder(order)If validation changes, the developer modifies one method.
Calculation Is Centralized
Order-total logic is implemented once:
calculateOrderTotal(order)Shipping Rule Has One Source of Truth
The free-shipping decision exists in:
calculateShippingCharge(total)A threshold change now affects all order sources consistently.
Magic Values Are Named
The values now communicate their meaning:
FREE_SHIPPING_THRESHOLD
STANDARD_SHIPPING_CHARGENo Unnecessary Design Pattern
A simple private-method refactoring solves the problem.
There is no need for:
- Abstract base classes
- Strategy pattern
- Template Method pattern
- Complex inheritance
unless future requirements introduce genuinely different processing behavior.
12. Bad Code vs Improved Code
| Aspect | Bad Code | Improved Code |
|---|---|---|
| Readability | Two large identical methods | Shared workflow with clear steps |
| Maintainability | Same change required multiple times | Business logic changed once |
| Testability | Repeated test scenarios | Common calculations can be tested centrally |
| Reliability | Easy for implementations to diverge | One authoritative implementation |
| Business rules | Duplicated thresholds | Centralized constants and methods |
| PR review | Reviewer compares multiple copies | Reviewer checks one implementation |
| Extensibility | Additional channels increase duplication | New channel can reuse shared workflow |
13. Real Project Scenario
Consider a healthcare notification system.
The application sends appointment reminders through:
- SMS
- Mobile push notifications
Three developers implement separate service methods:
sendEmailReminder()
sendSmsReminder()
sendPushReminder()Each method independently performs:
- Appointment validation
- Patient validation
- Consent verification
- Reminder eligibility check
- Time-zone conversion
- Message preparation
- Audit logging
Initially, all three implementations are copied from the same method.
Several months later, a regulatory requirement changes:
Reminders must not be sent if the patient has withdrawn communication consent.
The SMS implementation is updated.
The email and push implementations are accidentally missed.
Now the system behaves inconsistently and may send communications that should have been blocked.
The real duplication was not merely repeated Java code.
The duplicated knowledge included:
- Consent rules
- Reminder eligibility rules
- Audit requirements
A better architecture would centralize common eligibility decisions and keep only channel-specific delivery logic separate.
For example:
public void sendAppointmentReminder(Appointment appointment, NotificationChannel channel) {
validateReminderEligibility(appointment);
ReminderMessage message = reminderMessageFactory.create(appointment);
notificationSender.send(channel, message);
auditService.recordReminderSent(appointment, channel);
}Channel-specific communication remains separate, while common business rules are centralized.
14. Production Impact
Inconsistent Business Behavior
The most common production consequence is divergence.
One copy is changed while another remains outdated.
Example:
- Web orders receive free shipping at ₹6,000.
- Phone orders still receive free shipping at ₹5,000.
Repeated Bugs
A defect copied into five locations must be fixed five times.
Missing one copy leaves the production defect partially unresolved.
Incorrect Financial Calculations
Duplicated pricing, tax, commission, or discount logic is especially dangerous.
Different APIs may calculate different monetary results.
Difficult Debugging
Support teams may report:
The same customer receives different eligibility results depending on which API is called.
Developers then need to find and compare multiple implementations.
Maintenance Problems
Small business changes create disproportionately large development tasks because the same modification must be repeated.
Higher Regression Risk
Every additional implementation is another opportunity for:
- Typographical errors
- Incorrect conditions
- Missing edge cases
- Different rounding
- Different null handling
Performance Problems
If duplicated logic performs database queries or external service calls, different parts of the application may repeat expensive work unnecessarily.
Performance impact should be evaluated based on the duplicated operation rather than duplication itself.
15. Common Developer Mistakes
Copying Existing Methods to Save Time
A developer copies:
createCustomer()to create:
updateCustomer()and modifies a few lines.
Over time, shared validation and mapping logic diverge.
Overusing Utility Classes
Developers sometimes respond to duplication by moving everything into:
CommonUtilsThis creates a different maintainability problem: one unrelated global utility class with dozens of responsibilities.
Premature Abstraction
Two methods look similar, so the developer immediately creates a complex generic framework.
Later the two workflows evolve differently, and the abstraction becomes difficult to maintain.
Using Inheritance Only for Code Reuse
Two services contain similar methods, so a common parent class is introduced even though the services do not share a meaningful inheritance relationship.
Duplicating Business Constants
Values such as:
0.18
1000
5000
30appear throughout the project without explaining what they mean.
Duplicating Validation in Controllers
Several REST endpoints independently check:
- Null values
- Empty strings
- Identifier format
- Date boundaries
when the same validation could be represented more consistently.
Duplicate Mapping Code
Multiple controllers manually convert the same Customer entity to CustomerResponse.
Duplicate Exception Handling
Many methods repeat:
try {
...
} catch (Exception ex) {
log.error(...);
throw new ServiceException(...);
}without examining whether exception translation belongs at a more appropriate boundary.
Duplicating Repository Queries
Several repository methods express effectively the same query using slightly different names.
Blind DRY Refactoring
Developers extract code only because lines look similar without checking whether the code represents the same concept.
16. Edge Cases
Similar Code With Different Business Meaning
Suppose two services contain:
amount.compareTo(limit) > 0One checks a fraud threshold.
The other checks a premium-customer threshold.
The syntax is identical, but the business meaning is different.
These should not be combined merely because the code looks the same.
Logic That Is Currently Same but Expected to Diverge
Retail and corporate customer pricing may currently use identical calculations.
If the business has explicitly stated that the models will evolve separately, forcing them into one shared abstraction may create future coupling.
Null Handling Differences
Two duplicated methods may have different expectations:
- One treats null as invalid.
- Another treats null as "not provided."
Combining them without understanding this difference changes behavior.
Transaction Boundaries
Shared methods used from multiple Spring services may execute under different transactional contexts.
Refactoring must not accidentally change:
- Transaction propagation
- Lazy loading behavior
- Locking
- Rollback behavior
Security Context
Two similar methods may intentionally perform different authorization checks.
Removing "duplicate" security logic without understanding those differences can create vulnerabilities.
Concurrency
Shared mutable utility objects can introduce concurrency problems.
Reusable components should generally avoid storing request-specific mutable state.
Exception Behavior
Duplicated methods may throw different exceptions intentionally.
Consolidating them should not silently change API contracts.
17. Performance Considerations
Avoiding duplicate code is primarily a maintainability concern.
Extracting a few shared Java methods usually has no meaningful runtime cost.
The JVM can inline small frequently executed methods where appropriate.
However, reviewers should examine duplicate expensive operations.
Duplicate Database Queries
Suppose a service performs:
Customer customer = customerRepository.findById(customerId)
.orElseThrow();Then another helper independently executes the same lookup.
This may produce unnecessary queries.
Shared data should sometimes be passed to the method instead.
Duplicate External API Calls
Bad:
RiskResponse risk = fraudClient.check(customerId);executed separately by multiple validation methods during one request.
If the same response can safely be reused, calling the external service repeatedly wastes latency and downstream capacity.
Repeated Collection Processing
Bad:
orders.stream().filter(this::isCompleted).count();appearing several times in the same workflow.
If the collection is large, repeated traversal may increase CPU cost.
Duplicate Object Mapping
Large DTO mappings repeated across layers may increase object creation, although maintenance consistency is usually the larger concern.
Do Not Optimize Method Extraction
This:
private boolean isEligible(Customer customer) {
return customer.isActive() && !customer.isBlocked();
}does not create a meaningful performance concern compared with writing the expression inline.
Do not sacrifice readability to avoid trivial method calls.
18. Security Considerations
Duplicate code is not inherently a security vulnerability.
However, duplicated security rules are dangerous because copies can diverge.
Authorization Duplication
Suppose several controllers independently implement:
if (!currentUser.isAdmin() && !resource.getOwnerId().equals(currentUser.getId())) {
throw new AccessDeniedException("Access denied");
}If one implementation is changed incorrectly, the endpoints may enforce different access rules.
Authorization should normally have a clear and consistent ownership model.
Input Validation Duplication
Duplicated validation can lead to one endpoint accepting input rejected by another.
Sensitive Data Masking
If several logging paths independently mask customer data, one implementation may accidentally log:
- Email addresses
- Tokens
- Account numbers
- Personal information
Centralized, well-tested masking behavior may reduce that risk.
Security Bug Fixes
When a vulnerability is found in duplicated logic, reviewers must identify every copy.
This makes security remediation slower and less reliable.
Important Caution
Do not blindly centralize all security logic into a generic utility.
Security rules should remain:
- Explicit
- Domain-aware
- Testable
- Properly located in the security architecture
19. Testing Considerations
Refactoring duplicate code must preserve existing behavior.
Characterization Tests
Before removing duplication, create tests for the current implementations.
If both order channels are expected to behave identically, test that assumption.
Shared Business Logic Tests
For:
calculateShippingCharge(total)test:
- Below threshold
- Exactly at threshold
- Above threshold
Example:
@Test
void shouldChargeShippingWhenTotalIsBelowThreshold() {
BigDecimal charge = orderService.calculateShippingChargeForTest(new BigDecimal("4999.99"));
assertThat(charge).isEqualByComparingTo("200.00");
}Boundary Cases
Free-shipping threshold:
4999.995000.005000.01
Validation Tests
Verify:
- Null order
- Missing customer ID
- Empty items
- Null item price if applicable
- Invalid quantity if applicable
Workflow Tests
Verify that both:
processOnlineOrder()and:
processPhoneOrder()use the shared processing behavior correctly.
Interaction Tests
Verify:
orderRepository.save(...)and:
emailService.sendOrderConfirmation(...)are called as expected.
Regression Tests
If duplication is removed after an existing production bug, add a test specifically covering that bug.
Integration Tests
Use integration tests where duplicated code includes:
- Repository queries
- Transactions
- Database constraints
- External service adapters
20. Refactoring Guidelines
Step 1: Identify Real Duplication
Do not refactor based only on similar-looking lines.
Ask:
Do these implementations represent the same business knowledge?
Step 2: Compare All Copies
Check for subtle differences:
- Conditions
- Exceptions
- Null behavior
- Logging
- Transactions
- Side effects
- Security checks
Step 3: Write Tests
Protect current behavior before moving code.
Step 4: Extract the Smallest Meaningful Common Unit
For example:
calculateShippingCharge(total)rather than immediately creating an entire framework.
Step 5: Replace One Copy
Make one caller use the shared implementation.
Run tests.
Step 6: Replace Remaining Copies
Gradually remove duplicated implementations.
Step 7: Remove Dead Code
After all callers use the shared implementation, delete the old copies.
Step 8: Name the Abstraction Properly
Bad:
CommonUtils.calculate(...)Better:
ShippingPolicy.calculateCharge(...)if shipping policy is genuinely a reusable domain concept.
Step 9: Evaluate Ownership
Ask where the shared rule logically belongs:
- Entity
- Domain service
- Application service
- Validator
- Mapper
- Repository
- Dedicated policy class
Step 10: Avoid Over-Abstraction
If the extracted design becomes harder to understand than the duplication, reconsider it.
21. Best Practices
- Keep one source of truth for important business rules.
- Extract duplicate logic only after understanding its responsibility.
- Prefer domain-specific abstractions.
- Use meaningful method and class names.
- Centralize business thresholds where appropriate.
- Use enums for shared domain states.
- Reuse repository methods for equivalent queries.
- Create dedicated mapper components when mappings are widely reused.
- Use Bean Validation for consistent request validation where appropriate.
- Keep shared components cohesive.
- Prefer composition over inheritance for code reuse.
- Keep reusable components stateless unless state is genuinely required.
- Protect shared logic with focused unit tests.
- Document intentionally different implementations when they appear similar.
- Review duplicated code as a possible design signal, not merely a formatting problem.
22. Practices to Avoid
Blind Copy-Paste
Avoid copying an existing method and changing only a few lines.
It creates future divergence.
Generic CommonUtils
Avoid classes such as:
public class CommonUtils {
...
}containing unrelated:
- Date operations
- Customer validation
- Order calculations
- JSON conversion
- Payment logic
Such classes become maintenance dumping grounds.
Inheritance Only for Reuse
Avoid:
public class PaymentService extends CommonServiceonly because CommonService contains a few useful methods.
Inheritance should represent a valid relationship, not merely code sharing.
Overly Generic Methods
Avoid creating:
process(Object data, String type, boolean validate, boolean notify, boolean save)just to unify several workflows.
The method becomes more complex than the original duplication.
Premature Generalization
Two similar implementations are not enough evidence that a stable abstraction exists.
Shared Mutable State
Avoid reusable singleton Spring services containing mutable request-specific fields.
Hidden Business Knowledge
Do not move important rules into anonymous helper methods where the domain meaning becomes less obvious.
Large Base Classes
Avoid solving duplication by creating a superclass containing dozens of unrelated protected methods.
23. Code Review Checklist
- Is the same business rule implemented in multiple locations?
- Would these duplicated sections change for the same business reason?
- Is this duplication intentional or accidental?
- Are the copies already slightly different?
- Could a future change require modifying several files?
- Is validation duplicated across controllers or services?
- Is entity-to-DTO mapping repeated?
- Are the same repository queries implemented multiple times?
- Are business constants duplicated?
- Is exception-handling logic repeated unnecessarily?
- Are database calls duplicated during the same request?
- Are external API calls repeated unnecessarily?
- Would a private method remove the duplication cleanly?
- Does this logic belong in a dedicated domain component?
- Are we introducing an abstraction before understanding the common concept?
- Would inheritance be used only to reuse code?
- Is a generic utility class becoming a dumping ground?
- Does the proposed abstraction increase coupling?
- Are existing behavior differences covered by tests?
- Will removing duplication change transaction behavior?
- Will removing duplication affect security checks?
- Is the shared implementation easy to understand?
- Does the abstraction have clear ownership?
- Is the final code simpler than the duplicated version?
24. Common Pull Request Review Comments
- *This validation block is identical to the one in
CustomerUpdateService. Can we move the shared rule to one focused validator instead of maintaining two copies?*
- *The shipping calculation appears in three order flows. Since this is one business rule, please consider giving it a single implementation.*
- *These methods look similar, but before extracting them can we confirm that they are expected to evolve together? I want to avoid coupling two independent workflows.*
- *I would avoid adding this to
CommonUtils. The logic is specifically related to payment eligibility, so a domain-oriented component would provide clearer ownership.*
- *This query duplicates the existing repository method. Can we reuse the existing method so query changes remain centralized?*
- *The entity-to-response mapping is now implemented in multiple controllers. A dedicated mapper would reduce duplication and keep response construction consistent.*
- *Please add characterization tests before removing this duplication. The two implementations currently differ in exception behavior.*
- *I don't think we need inheritance just to share these three lines. Composition or a private helper method would keep the design simpler.*
- *The same threshold is hardcoded in several services. Can we move the business value to one authoritative location?*
- *This external API call is already performed earlier in the workflow. Can we pass the result instead of calling the downstream service again?*
25. Code Review Exercise
Review the following Spring Boot service.
Identify:
- Problems
- Code smells
- Risks
- Improvements
Do not read the solution until completing your review.
@Service
public class RefundService {
private final PaymentRepository paymentRepository;
private final RefundRepository refundRepository;
private final NotificationService notificationService;
public RefundService(PaymentRepository paymentRepository, RefundRepository refundRepository, NotificationService notificationService) {
this.paymentRepository = paymentRepository;
this.refundRepository = refundRepository;
this.notificationService = notificationService;
}
public Refund refundCardPayment(Long paymentId, BigDecimal refundAmount) {
Payment payment = paymentRepository.findById(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment not found"));
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid refund amount");
}
if (refundAmount.compareTo(payment.getAmount()) > 0) {
throw new IllegalArgumentException("Refund cannot exceed payment amount");
}
if (payment.getStatus() != PaymentStatus.COMPLETED) {
throw new IllegalStateException("Only completed payments can be refunded");
}
Refund refund = new Refund();
refund.setPaymentId(paymentId);
refund.setAmount(refundAmount);
refund.setStatus(RefundStatus.APPROVED);
refund.setCreatedAt(Instant.now());
Refund savedRefund = refundRepository.save(refund);
notificationService.sendRefundConfirmation(payment.getCustomerId(), savedRefund);
return savedRefund;
}
public Refund refundUpiPayment(Long paymentId, BigDecimal refundAmount) {
Payment payment = paymentRepository.findById(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment not found"));
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid refund amount");
}
if (refundAmount.compareTo(payment.getAmount()) > 0) {
throw new IllegalArgumentException("Refund cannot exceed payment amount");
}
if (payment.getStatus() != PaymentStatus.COMPLETED) {
throw new IllegalStateException("Only completed payments can be refunded");
}
Refund refund = new Refund();
refund.setPaymentId(paymentId);
refund.setAmount(refundAmount);
refund.setStatus(RefundStatus.APPROVED);
refund.setCreatedAt(Instant.now());
Refund savedRefund = refundRepository.save(refund);
notificationService.sendRefundConfirmation(payment.getCustomerId(), savedRefund);
return savedRefund;
}
}Review the code for:
- Duplicate workflow
- Duplicate validation
- Domain ownership
- Maintainability
- Future payment-method differences
- Testing implications
- Appropriate level of abstraction
26. Exercise Solution
Problems Identified
1. Almost Complete Method Duplication
refundCardPayment and refundUpiPayment contain the same implementation.
The only difference is the method name.
2. Duplicate Payment Lookup
Both methods perform:
paymentRepository.findById(paymentId)using identical exception behavior.
3. Duplicate Refund Validation
The following rules are duplicated:
- Refund amount must be positive.
- Refund cannot exceed payment amount.
- Payment must be completed.
These are core refund-domain rules.
4. Duplicate Refund Construction
Both methods construct:
Refundin exactly the same way.
5. Duplicate Persistence and Notification
Both methods:
- Save the refund.
- Send the same confirmation.
6. Future Divergence Risk
If the business later changes refund rules for all payment types, multiple methods must be updated.
7. Payment Type Is Not Actually Used
Despite the method names, neither method verifies whether the payment is:
- Card
- UPI
This may itself be a business bug.
Improved Code
@Service
public class RefundService {
private final PaymentRepository paymentRepository;
private final RefundRepository refundRepository;
private final NotificationService notificationService;
private final Clock clock;
public RefundService(PaymentRepository paymentRepository, RefundRepository refundRepository, NotificationService notificationService, Clock clock) {
this.paymentRepository = paymentRepository;
this.refundRepository = refundRepository;
this.notificationService = notificationService;
this.clock = clock;
}
public Refund refundCardPayment(Long paymentId, BigDecimal refundAmount) {
return processRefund(paymentId, refundAmount, PaymentMethod.CARD);
}
public Refund refundUpiPayment(Long paymentId, BigDecimal refundAmount) {
return processRefund(paymentId, refundAmount, PaymentMethod.UPI);
}
private Refund processRefund(Long paymentId, BigDecimal refundAmount, PaymentMethod expectedPaymentMethod) {
Payment payment = findPayment(paymentId);
validatePaymentMethod(payment, expectedPaymentMethod);
validateRefund(payment, refundAmount);
Refund refund = createRefund(paymentId, refundAmount);
Refund savedRefund = refundRepository.save(refund);
notificationService.sendRefundConfirmation(payment.getCustomerId(), savedRefund);
return savedRefund;
}
private Payment findPayment(Long paymentId) {
return paymentRepository.findById(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment not found"));
}
private void validatePaymentMethod(Payment payment, PaymentMethod expectedPaymentMethod) {
if (payment.getPaymentMethod() != expectedPaymentMethod) {
throw new IllegalArgumentException("Payment method does not match refund operation");
}
}
private void validateRefund(Payment payment, BigDecimal refundAmount) {
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid refund amount");
}
if (refundAmount.compareTo(payment.getAmount()) > 0) {
throw new IllegalArgumentException("Refund cannot exceed payment amount");
}
if (payment.getStatus() != PaymentStatus.COMPLETED) {
throw new IllegalStateException("Only completed payments can be refunded");
}
}
private Refund createRefund(Long paymentId, BigDecimal refundAmount) {
Refund refund = new Refund();
refund.setPaymentId(paymentId);
refund.setAmount(refundAmount);
refund.setStatus(RefundStatus.APPROVED);
refund.setCreatedAt(Instant.now(clock));
return refund;
}
}Why Each Change Is Useful
Shared Processing Method
The common workflow now exists once:
processRefund(...)Payment Method Is Explicit
The two public methods retain meaningful APIs while passing:
PaymentMethod.CARDor:
PaymentMethod.UPIThe service now verifies that the operation matches the actual payment type.
Validation Is Centralized
Refund rules exist in one method:
validateRefund(...)Future business-rule changes are applied once.
Lookup Is Centralized
Payment retrieval and exception behavior are consistent.
Refund Creation Is Centralized
Object construction occurs in one place.
Clock Is Injected
Using:
Instant.now(clock)makes time-dependent behavior easier to test than calling Instant.now() directly.
Important Design Note
If card and UPI refunds later require fundamentally different workflows—for example:
- Different payment gateways
- Different settlement periods
- Different external APIs
- Different retry behavior
then simply adding many if statements to processRefund may become inappropriate.
At that point, separate payment-method strategies or dedicated processors may become justified.
The abstraction should evolve only when the real variation exists.
27. Interview Perspective
Avoiding duplicate code frequently appears in senior Java and code-review interviews because the interviewer wants to evaluate design judgment.
The question is rarely:
What does DRY mean?
A more realistic question is:
You find the same 30 lines in three Spring services. What would you do?
A strong candidate should not immediately answer:
Move everything into a utility class.
Instead, the candidate should analyze:
- What behavior is duplicated?
- Does the duplication represent the same business knowledge?
- Where should that responsibility belong?
- Are there subtle differences?
- Will the implementations evolve together?
- What tests protect the behavior?
- What is the simplest useful abstraction?
Spring Boot Perspective
Duplicate code may involve:
- Controllers
- Service methods
- Repository access
- Transaction handling
- DTO mapping
- Exception handling
- Feign/WebClient integration
The interviewer may expect knowledge of appropriate Spring mechanisms such as:
@ControllerAdvice- Bean Validation
- Reusable Spring components
- Repository methods
- Mappers
- Domain services
However, these should only be used when they fit the responsibility.
Senior Developer Perspective
Senior developers are expected to understand the tension between:
- Duplication
- Abstraction
- Coupling
- Maintainability
Removing duplication is not always automatically an improvement.
A poor abstraction can be more expensive than a small amount of duplication.
28. Interview Questions and Answers
Basic Question
Question: What is duplicate code, and why is it a problem?
Answer:
Duplicate code occurs when the same or substantially similar logic is implemented in multiple places. It becomes a maintenance problem because bug fixes and business-rule changes must be applied consistently to every copy. If one implementation is missed, system behavior can diverge.
Intermediate Question
Question: You find the same validation logic in five Spring Boot controllers. How would you improve it?
Answer:
First, I would verify that all five endpoints genuinely share the same validation rule. Depending on the validation, I might use Bean Validation annotations, a custom constraint validator, or a focused domain validator. I would avoid moving unrelated validation into a generic utility class. The goal is to give one business rule one authoritative implementation.
Advanced Question
Question: Is duplicate code always worse than abstraction?
Answer:
No. A small amount of duplication can be preferable to a wrong abstraction. If two pieces of code only happen to look similar but represent different business concepts or are likely to evolve independently, combining them may create unnecessary coupling. I normally remove duplication when I understand the shared responsibility, not simply because lines look alike.
Scenario-Based Question
Question: Three microservices implement the same customer-eligibility rule. What would you do?
Answer:
I would first determine whether that rule truly belongs to one domain and whether the microservices should independently own it. If the rule must remain globally consistent, options might include assigning ownership to the appropriate domain service, exposing it through an API, publishing authoritative data, or sharing a versioned library where appropriate. I would not automatically copy a shared JAR across every service because that can create deployment coupling. The decision depends on service boundaries and ownership.
Code-Review Question
Question: A developer extracts three repeated lines into CommonUtils. What would you review?
Answer:
I would check whether the extracted logic represents a clear domain concept. If it does, I would prefer a name and location that reflects that concept, such as ShippingPolicy or RefundValidator. A generic CommonUtils class often becomes a collection of unrelated methods and weakens ownership.
Real-Project Question
Question: How have you handled duplication that appeared across several service classes?
Answer:
I first compared the implementations to identify whether they represented the same responsibility or only similar syntax. For truly shared business rules, I moved the behavior to a focused component and added tests around it. For duplicated infrastructure logic, I considered mechanisms such as mappers, exception handlers, or shared adapters. I avoided creating a common abstraction when the business workflows were expected to evolve independently.
29. Quick Rule to Remember
Remove duplicated knowledge, not merely duplicated lines.
30. Final Takeaway
What the Developer Should Remember
Duplicate code becomes dangerous when several pieces of code represent the same business rule.
If changing one rule requires searching the entire project for copied implementations, the codebase probably lacks a clear source of truth.
Developers should:
- Identify duplication early.
- Understand the common responsibility.
- Extract the smallest meaningful abstraction.
- Keep business rules in one authoritative location.
- Use domain-oriented names.
- Protect shared logic with tests.
- Avoid copy-paste development.
- Avoid unnecessary inheritance.
- Avoid generic utility dumping grounds.
- Accept small duplication when abstraction would create worse coupling.
The objective is not to produce the fewest possible lines of Java.
The objective is to create a system in which important behavior has clear ownership.
What the Reviewer Should Check
During Pull Request review, ask:
- Is this logic already implemented elsewhere?
- Does this code represent the same business knowledge?
- Could the copies diverge after the next requirement change?
- Is there an existing reusable component?
- Would a private method be enough?
- Does the logic deserve a dedicated domain component?
- Are we creating unnecessary abstraction?
- Are subtle behavioral differences being preserved?
- Are tests protecting the refactoring?
- Does the resulting design remain easy to understand?
What Should Be Avoided in Production Code
Avoid repeatedly copying:
- Business rules
- Validation rules
- Pricing calculations
- Security decisions
- Mapping logic
- Repository behavior
- External integration workflows
Do not solve every duplication problem using:
CommonUtilsDo not introduce inheritance purely for code reuse.
Do not create complex generic frameworks just to remove a few similar methods.
Do not assume two implementations should be merged simply because they currently look alike.
The preferred approach is:
Understand why the duplication exists, identify the real shared responsibility, create one clear source of truth, and keep the resulting design simpler than the duplicated code it replaces.