Spaghetti & Dead code
- Anand Nerurkar
- 1 day ago
- 6 min read
Spaghetti code is messy, tangled code that’s hard to read, understand, change, or test — like a bowl of pasta where everything’s twisted together. 🍝
What it is
Spaghetti code is code with:
Poor structure (no clear modules or separation of concerns)
Long, monolithic functions or methods
Lots of global/shared state
Tight coupling and hidden dependencies
Lots of copy-paste / duplication
Minimal or no tests and no documentation
Why it's bad
Hard to understand and maintain
Changes introduce bugs in unexpected places
Difficult to add features or refactor safely
Slows team velocity and increases technical debt
Common causes
Rushed deadlines / lack of design
No code reviews or standards
Single dev working alone for long time
Features tacked onto existing code without refactor
Lack of tests or CI
Signs you have spaghetti code
One file/class contains hundreds or thousands of lines
Methods > 200 lines or > 10 responsibilities
Many if/else/switch ladders spread across codebase
Hard-to-track global variables or singletons used everywhere
Repeated code blocks across modules
How to fix / refactor (practical checklist)
Write tests first — add small unit/integration tests around current behavior so you can refactor safely.
Identify boundaries — find logical responsibilities, group related functionality.
Extract methods — break long functions into smaller named methods.
Apply SRP — each class/method should have one responsibility.
Introduce interfaces — decouple implementation from usage.
Move toward layers — controller/service/repo (or equivalent).
Replace duplication — extract common utilities or services.
Introduce patterns where appropriate (Factory, Strategy, Adapter, etc.) — not overengineer.
Refactor incrementally with small commits and CI runs.
Add documentation & code comments for gotchas.
Enforce code review & linters to prevent regression.
Example (Java - quick illustration)
Bad / spaghetti-style:
public class LoanProcessor {
public String process(InputData d) {
// 200+ lines mixing validation, DB, business rules, email, logging...
if (!d.isValid()) return "error";
// direct JDBC calls here...
// complex nested ifs for eligibility, interest calculation, fee updates
// send email by creating socket / SMTP code inline
return "approved";
}
}
Refactored (cleaner):
public class LoanController {
private final LoanService loanService;
public Response applyLoan(Request req) {
return loanService.process(req);
}
}
public class LoanService {
private final Validator validator;
private final LoanRepository repo;
private final NotificationService notifier;
public Response process(Request r) {
validator.validate(r);
var domain = LoanFactory.createFrom(r);
var result = eligibilityCalculator.check(domain);
repo.save(domain);
notifier.sendResult(domain, result);
return result.toResponse();
}
}
Each class does one thing; responsibilities are clear and testable.
Tools & metrics to help
Static analysis: SonarQube, Checkstyle, PMD, ESLint (for JS)
Complexity metrics: cyclomatic complexity, method length
Dependency analyzers: JDepend, IntelliJ dependency tools
Refactor-friendly IDEs: IntelliJ, VS Code
Automated tests & CI (JUnit, Mockito, etc.)
Team/process fixes
Establish code style + architecture guidelines
Mandatory PR reviews and pair-programming for tricky areas
Keep PRs small and atomic
Invest in automated test coverage and CI
Schedule regular refactor sprints for tech debt
Quick rule of thumb
If you can’t explain what a function/class does in one sentence, it probably needs refactoring.
Want help? I can:
Review a short messy function you paste and show a refactor
Give a prioritized refactoring plan for a module or service
Generate unit tests for a risky piece of code
Dead code is any piece of code that is written but never used or executed.It "sits" in the codebase, increases complexity, but has no impact on program behavior.
🔎 Types of Dead Code
Unreachable code – Code that can never be executed because of logic/flow.
public int getValue() { return 5; System.out.println("This will never run"); // unreachable }
Unused code – Methods, classes, variables, or constants that are defined but never referenced.
class LoanService { private int unusedCounter; // never used }
Redundant logic – Duplicated or legacy logic replaced by newer code but not deleted.
if (x > 10) { // old logic, never actually executed due to earlier conditions }
Commented-out code – Often considered “dead” if left in source for too long.
// processLoan(request); // old logic, replaced but not removed
⚠️ Why Dead Code is Bad
Increases maintenance cost – Developers waste time reading unused code.
Confuses team – Harder to understand which code is “real” logic.
Larger binaries – Can bloat compiled artifacts (in some cases).
Hides bugs – Sometimes dead code looks “important,” leading to mistakes.
✅ How to Detect and Remove
Static analysis tools: SonarQube, PMD, Checkstyle, ESLint (for JS), FindBugs.
Compiler warnings: Many compilers warn for unreachable/unused variables or imports.
Code coverage tools: Jacoco, Istanbul, Coverage.py → show which lines never execute.
Code reviews: Encourage removing instead of commenting out code.
🧹 Best Practices
Delete unused methods/classes instead of keeping them “just in case” → source control (Git) keeps history.
Regularly run linting + coverage reports in CI.
Apply YAGNI principle (“You Aren’t Gonna Need It”) — don’t write code you think you’ll need later.
If you must keep it temporarily, mark clearly with @Deprecated (Java) or comments + ticket reference.
👉 Quick rule:Dead code = Code that adds no value but adds burden. If in doubt, delete —
🥊 Dead Code vs. Spaghetti Code
Aspect | Dead Code | Spaghetti Code |
Definition | Code that is written but never used/executed. | Code that is messy, tangled, unstructured, and hard to follow. |
Examples | - Unused variables, methods, classes - Unreachable statements - Commented-out legacy code | - Very long functions - Lots of nested if/else or switch statements - Mixing DB, business logic, and UI in one place |
Impact | - Wasted space - Confuses developers (“Do we need this?”) - Slows down reading/understanding | - Hard to debug & maintain - Small changes break unrelated areas - Slows feature delivery |
When It Happens | - Old features removed but code left behind - Developers write “future use” code that never gets used - Refactoring leftovers | - No proper design upfront - Quick fixes & patches over time - Lack of modularization and separation of concerns |
Detection | - Static analysis (SonarQube, PMD, ESLint) - Compiler warnings - Code coverage reports | - Harder to auto-detect - Symptoms: high cyclomatic complexity, long classes/functions, low cohesion |
Fix | - Delete unused code (Git keeps history) - Use @Deprecated if you must keep it temporarily | - Refactor into smaller, testable modules - Apply SOLID principles - Add proper layers & patterns |
Analogy | A dead branch on a tree 🌿 (it’s there but lifeless, does nothing) | A tangled bowl of spaghetti 🍝 (everything mixed, can’t pull one strand cleanly) |
✅ Quick Rule
Dead Code = Unused (safe to delete).
Spaghetti Code = Used but messy (must be refactored).
Let’s take a Digital Lending use case (Loan Processing Service) and show both Dead Code and Spaghetti Code inside it.
🧑💻 Example: Loan Processing Service
1️⃣ Spaghetti Code (messy but used)
public class LoanProcessor {
public String processLoan(Application app) {
// Mix of validation, DB, business rules, notifications - all tangled
if (app.getCustomerAge() < 18) {
return "Rejected: Underage";
} else {
if (app.getCreditScore() < 650) {
if (app.getSalary() > 50000) {
// apply special rule
return "Pending: Manual Review";
} else {
return "Rejected: Low Credit Score";
}
} else {
// inline DB call
try {
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/loans");
PreparedStatement ps = conn.prepareStatement("INSERT INTO loans VALUES (?, ?)");
ps.setInt(1, app.getId());
ps.setString(2, "APPROVED");
ps.execute();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
// inline email notification
System.out.println("Sending approval email to: " + app.getEmail());
return "Approved";
}
}
}
// Dead code below (never used anywhere)
public void legacyLoanRule(Application app) {
System.out.println("Old scoring model: Not used anymore!");
}
}
2️⃣ Where’s the Problem?
Spaghetti Code
processLoan does everything: validation, DB, business rules, notification.
Nested if-else = hard to extend.
No separation of concerns.
Dead Code
legacyLoanRule() is never called.
Still in the class, but contributes nothing.
Confuses developers (“Should I call this?”).
3️⃣ Refactored (Clean Code)
public class LoanProcessor {
private final LoanValidator validator;
private final LoanRepository repo;
private final NotificationService notifier;
public LoanProcessor(LoanValidator validator, LoanRepository repo, NotificationService notifier) {
this.validator = validator;
this.repo = repo;
this.notifier = notifier;
}
public String processLoan(Application app) {
validator.validate(app);
String status = evaluateLoan(app);
repo.save(app.getId(), status);
notifier.send(app.getEmail(), status);
return status;
}
private String evaluateLoan(Application app) {
if (app.getCustomerAge() < 18) return "Rejected: Underage";
if (app.getCreditScore() < 650 && app.getSalary() <= 50000) return "Rejected: Low Credit Score";
if (app.getCreditScore() < 650) return "Pending: Manual Review";
return "Approved";
}
}
✅ Now:
Spaghetti removed → responsibilities are split into validator, repo, notifier.
Dead code deleted → no unused legacy methods cluttering code.
Let’s simulate how SonarQube (or any static analysis tool) would flag Dead Code and Spaghetti Code in a Digital Lending Service.
🔎 Example: SonarQube Report for LoanProcessor.java
1️⃣ Spaghetti Code Findings
Rule: Cognitive Complexity > 15
File: LoanProcessor.java
Method: processLoan(Application)
Line: 5
Severity: MAJOR
Message: Method has a Cognitive Complexity of 35 which is greater than 15 authorized. Refactor this method to reduce its complexity.
Rule: Long Method
File: LoanProcessor.java
Method: processLoan(Application)
Line: 5
Severity: MAJOR
Message: Method has 80 lines. Maximum allowed is 20. Split this method into smaller methods.
Rule: S1643 (Mixing concerns)
File: LoanProcessor.java
Method: processLoan(Application)
Line: 25
Severity: CRITICAL
Message: Avoid direct database and notification calls inside business logic. Use repository and service layers.
2️⃣ Dead Code Findings
Rule: Unused Method
File: LoanProcessor.java
Method: legacyLoanRule(Application)
Line: 60
Severity: MINOR
Message: Remove this unused method or reference it. If it is intended for future use, mark it with @Deprecated.
Rule: Dead Store
File: LoanProcessor.java
Variable: unusedCounter
Line: 12
Severity: MINOR
Message: Remove this unused variable.
3️⃣ How Tools Differentiate
Spaghetti Code → flagged by rules around complexity, long methods, mixing concerns, code smells.
Dead Code → flagged by rules around unused methods, unreachable code, unused variables.
4️⃣ Team Workflow
CI pipeline (e.g., Azure DevOps, GitHub Actions, Jenkins) runs SonarQube scan on each PR.
Dashboard shows quality gates:
Max method complexity ≤ 15
Coverage ≥ 80%
No blocker or critical issues before merge
Developers fix flagged code before merging → keeps repo clean.
✅ So in practice:
Spaghetti Code → You’d see “complexity too high” warnings.
Dead Code → You’d see “unused method/variable” warnings.
Comments