By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(For Agile & Scrum Teams in Production)
You’re a Scrum team lead. Your sprint demo goes well—new features work, stakeholders are happy. But behind the scenes, your codebase is a mess: - Technical debt is piling up: quick fixes, duplicated logic, and "temporary" workarounds that never got cleaned up.- Refactoring is treated like a luxury, not a necessity—so every new feature takes longer to implement.- CI pipelines are flaky, breaking on every third commit, and no one trusts the build.
What breaks if you ignore this?- Velocity crashes: Your team’s sprint capacity drops by 30-50% because every change requires untangling spaghetti code.- Outages increase: A "simple" bug fix introduces three new ones because the codebase is too fragile.- Morale plummets: Developers dread touching legacy code, and your best engineers start looking for jobs.
What’s the superpower?- Predictable delivery: Refactored code + reliable CI = faster, safer releases.- Lower stress: No more "we can’t touch that module" fear.- Happier stakeholders: Features ship on time, and quality improves.
Real-world scenario:You inherit a 5-year-old monolith with: - No tests.- 200-line methods.- A CI pipeline that runs once a week (and fails 50% of the time).Your CTO says: "We need to ship a critical feature in 2 sprints. Can you do it?" This guide is your playbook.
Definition: The cost of future rework caused by choosing a quick, suboptimal solution now instead of a better approach that would take longer.Production insight:- Not all debt is bad—like financial debt, it can fund growth (e.g., shipping an MVP to validate a market).- But unmanaged debt compounds—like credit card interest, it slows you down exponentially.
Definition: Improving the internal structure of code without changing its external behavior.Production insight:- Refactoring ≠ rewriting—you’re not throwing away the old code, just making it cleaner.- Tests are mandatory—if you refactor without tests, you’re just guessing.
Definition: Surface indicators that suggest deeper problems in the code.Examples:- Long methods (>20 lines) → Hard to read, test, and modify.- Duplicated code → Changes must be made in multiple places.- Magic numbers/strings → Hardcoded values with no explanation.- God objects → One class does everything.Production insight:- Fix smells early—they’re like mold: small patches are easy to clean, but a full infestation requires a rewrite.
Definition: The practice of merging code changes into a shared branch frequently (at least daily), with automated builds and tests.Production insight:- CI ≠ just running tests—it’s about failing fast so you catch issues before they reach production.- A broken CI pipeline is a blocked team—if the build is red, no one can merge.
Definition: A branching strategy where developers work on small, short-lived branches (or directly on main) and merge frequently.Production insight:- Long-lived branches = merge hell—the longer a branch lives, the harder it is to merge.- TBD forces small, incremental changes—which are easier to review and test.
main
Definition: A technique to toggle features on/off at runtime without deploying new code.Production insight:- Decouples deployment from release—you can merge code to main without exposing it to users.- Enables safe refactoring—you can refactor behind a flag and toggle it off if something breaks.
Definition: A strategy for balancing different types of tests: - Unit tests (fast, isolated) → Integration tests (interactions between components) → E2E tests (slow, user flows).Production insight:- Aim for 70% unit tests, 20% integration, 10% E2E—this keeps your pipeline fast and reliable.
Definition: The steps your code goes through in CI: 1. Linting (code style checks).2. Unit tests (fast feedback).3. Integration tests (database, APIs).4. Build (compile, package).5. Deploy to staging (optional).6. E2E tests (optional, slow).Production insight:- Parallelize stages—if unit tests take 2 mins and integration tests take 5, run them in parallel to save time.
Goal: Quantify the problem so you can track progress.
# Example: Run SonarQube (local Docker setup) docker run -d --name sonarqube -p 9000:9000 sonarqube:community # Then analyze your code (replace with your project path) sonar-scanner \ -Dsonar.projectKey=my-project \ -Dsonar.sources=. \ -Dsonar.host.url=http://localhost:9000 \ -Dsonar.login=<your-token>
Expected output:- A dashboard showing: - Code smells (e.g., 42 long methods, 15 duplicated blocks). - Test coverage (e.g., 35%). - Security vulnerabilities (e.g., hardcoded secrets).
UserService
PaymentProcessor
Goal: Decide what to fix now vs. later.
Action:- Fix "Reckless Deliberate" debt first (e.g., no tests, duplicated logic).- Schedule "Prudent Deliberate" debt (e.g., refactoring after a major release).
Goal: Prove that refactoring works without breaking everything.
MainController
Goal: Lock in current behavior so you can refactor safely.
# Example: Python characterization test def test_user_service_returns_expected_data(): user_service = UserService() result = user_service.get_user(123) # Save the current output (even if it's wrong) assert result == {"id": 123, "name": "Alice", "role": "admin"}
Why this works:- You’re not testing correctness—you’re testing consistency.- If the test fails after refactoring, you broke something.
Example: Extract a MethodBefore:
def process_order(order): # Validate order if not order["items"]: raise ValueError("No items") if order["total"] < 0: raise ValueError("Negative total") # Calculate tax tax = order["total"] * 0.08 # Save to DB db.save(order)
After:
def _validate_order(order): if not order["items"]: raise ValueError("No items") if order["total"] < 0: raise ValueError("Negative total") def _calculate_tax(total): return total * 0.08 def process_order(order): _validate_order(order) tax = _calculate_tax(order["total"]) db.save(order)
Key moves:1. Extract methods to reduce line count.2. Rename variables for clarity (e.g., tax_rate = 0.08).3. Remove magic numbers (e.g., TAX_RATE = 0.08).
tax_rate = 0.08
TAX_RATE = 0.08
# Run tests after each small change pytest tests/test_user_service.py -v
Expected output:- All tests pass (or fail in a way you expect).
Goal: Make the pipeline fast, reliable, and trustworthy.
# Example: GitHub Actions workflow file (.github/workflows/ci.yml) name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install - run: npm test # This is too slow!
Problems:- No parallelization → Tests run sequentially.- No caching → npm install runs every time.- No linting → Style issues slip through.
npm install
name: CI on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install - run: npm run lint # Fast (10s) test-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install - run: npm run test:unit # Fast (30s) test-integration: runs-on: ubuntu-latest needs: test-unit # Run only if unit tests pass steps: - uses: actions/checkout@v4 - run: npm install - run: npm run test:integration # Slower (2m) build: runs-on: ubuntu-latest needs: [lint, test-unit, test-integration] steps: - uses: actions/checkout@v4 - run: npm install - run: npm run build
Key improvements:1. Parallel jobs → Linting and unit tests run at the same time.2. Caching → Add actions/cache to speed up npm install.3. Fail fast → Integration tests only run if unit tests pass.
actions/cache
Require status checks to pass
git-secrets
bash git secrets --install git secrets --add 'password' git secrets --add 'aws_access_key_id'
runs-on: ubuntu-latest
feature/fix-login-bug
fix/123
tech-debt
refactor
bug
Correct answer: "Allocate 10-20% of sprint capacity to debt reduction, and track it in the backlog."
"What’s the best way to refactor a legacy system?"
Correct answer: "Refactor incrementally with tests, using feature flags to decouple deployment from release."
"Why is CI important in Agile?"
"Your team’s velocity is dropping because of technical debt. What’s the first step?"- Trap answer: "Add more developers." - Correct answer: "Measure the debt (e.g., with SonarQube), prioritize it using the Debt Quadrant, and allocate sprint capacity to fix it."
Challenge:You have a Python function that’s 50 lines long and does 3 things: 1. Validates input.2. Processes data.3. Saves to a database.
Task:Refactor it into 3 smaller functions, write a characterization test, and verify the behavior doesn’t change.
Solution:
# Before def process_data(data): if not data["id"]: raise ValueError("No ID") if data["value"] < 0: raise ValueError("Negative value") # ... 40 more lines of processing ... db.save(data) return data # After def _validate_data(data): if not data["id"]: raise ValueError("No ID") if data["value"] < 0: raise ValueError("Negative value") def _process_data(data): # ... processing logic ... return processed_data def process_data(data): _validate_data(data) processed_data = _process_data(data) db.save(processed_data) return processed_data # Test def test_process_data(): data = {"id": 1, "value": 10} result = process_data(data) assert result == {"id": 1, "value": 10, "processed": True} # Lock in current behavior
Why it works:- Smaller functions = easier to test and modify.- Characterization test = ensures behavior doesn’t change.
Final Thought:Technical debt isn’t a "someday" problem—it’s a today problem. The longer you ignore it, the more it costs. Start small: pick one module, write tests, refactor it, and make your CI pipeline unbreakable. Your future self (and your team) will thank you. ?
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.