Frequently Asked Questions
Everything you need to know about the Software Craftsmanship Course
General
Discover Framework
Adapt Framework
Track Framework
Practical Questions
General Software Development FAQ
Technical Debt, Code Complexity & Design Principles
These frequently asked questions are based on real challenges faced by developers at companies ranging from startups to tech giants like Google and Amazon. Whether you're a junior developer trying to understand your first large codebase or a senior engineer looking to prevent your system from slowing down, you'll find practical answers here.
Technical debt represents the future cost of taking shortcuts in software development today. Like financial debt, it accumulates interest over time. What starts as a quick fix to meet a deadline can slow your entire team down months or years later.
According to Google's research on technical debt, a substantial percentage of their engineers reported being significantly hindered by "unnecessary complexity and technical debt" in their quarterly surveys. The longer technical debt remains unaddressed, the more expensive it becomes to fix.
Why it matters: Technical debt directly impacts your productivity, your team's velocity, and ultimately your career growth. Systems with high technical debt take longer to modify, have more bugs, and make it harder to deliver new features. As a developer, learning to identify and manage technical debt early is one of the most valuable skills you can develop.
Google's research identified 10 common categories of technical debt through systematic surveys of their engineering teams:
- Migration needs: Systems requiring updates to new platforms or services
- Code degradation: Code quality declining over time
- Dead code: Unused code still in the codebase
- Lack of documentation: Missing or outdated documentation
- Insufficient testing: Inadequate test coverage
- Code rot: Code that hasn't kept up with evolving standards
- Team expertise gaps: Knowledge concentrated in few people
- Dependency debt: Outdated or poorly managed dependencies
Red flags to watch for:
- Features that used to take days now take weeks
- Fear of changing certain parts of the codebase
- New team members taking months to become productive
- Frequent bugs in the same areas
- Code that no one fully understands
The key distinction Google makes is measuring not just what debt exists, but what debt actively slows developers down. This helps prioritize what to address first.
Code complexity comes in several forms, and understanding each helps you manage them effectively:
1. Cyclomatic Complexity: Measures the number of independent paths through code. A function with many if statements, loops, and branches has high cyclomatic complexity. Keep this under 10 per function for maintainable code.
2. Cognitive Complexity: Measures the mental effort required to understand code. Even low cyclomatic complexity can have high cognitive complexity if the logic is difficult to follow. Companies like HTEC report cognitive complexity up to 11x lower than industry averages through focused attention to this metric.
3. Halstead Complexity: Analyzes code vocabulary and structure - how many unique operators and operands exist and how they're used. Helps gauge the intellectual effort needed to work with the code.
4. Structural Complexity:
- Depth of Inheritance: Deep inheritance hierarchies make code harder to understand and modify
- Coupling: How interdependent your components are - high coupling means changes cascade unpredictably
- Cohesion: How well a module's components work together toward a single purpose
Why this matters: High complexity isn't always bad - some problems are genuinely complex. But accidental complexity from poor design is the killer. It makes code harder to test, maintain, and modify safely. Most production issues trace back to complexity that wasn't managed properly.
Tools to track these: SonarQube, Checkmarx, and PMD can automatically measure complexity metrics in your codebase.
You're not alone - this is incredibly common, and it's not because you're not smart enough. The problem is that most developers aren't taught systematic approaches to understanding code.
The typical approach (that doesn't work):
- Randomly browsing files hoping something clicks
- Keyword searching for specific terms
- Adding print statements everywhere
- Following data flows without understanding the big picture
- Constantly interrupting teammates with questions
This "bottom-up" exploration is exhausting and ineffective. You build a fragile mental model that evaporates after two weeks, forcing you to start over next time you work in that area.
What makes codebases hard to understand:
- Poor abstractions: When modules don't have clear responsibilities
- Hidden dependencies: Implicit connections that aren't documented
- Tribal knowledge: Critical context that exists only in people's heads
- Inconsistent patterns: Different parts solving similar problems differently
- Lack of documentation: Or worse, outdated documentation that misleads you
This is one of the most important judgment calls in software development. Refactor too little, and technical debt compounds. Refactor too much, and you waste time gold-plating code that doesn't need it.
Refactor when:
- You need to change the code anyway: The "Boy Scout Rule" - leave code cleaner than you found it when you're already working there
- The same bugs keep appearing: Sign of a structural problem that needs fixing
- Changes are taking too long: If simple modifications require touching many files
- New features are painful: The design actively fights what you're trying to build
- Code is hard to test: Usually indicates tight coupling or unclear responsibilities
- Team velocity is declining: Multiple developers struggling with the same areas
Leave code alone when:
- It's working and stable: If it's not causing problems, don't introduce risk
- You don't understand the domain yet: Premature abstraction is often worse than duplication
- It's isolated code: Not spreading complexity to other areas
- Low change frequency: Code that rarely needs modification doesn't need to be perfect
- No clear improvement: Can you articulate exactly what would be better?
Red flags - don't refactor:
- "I'll make it more elegant" (ego-driven, no business value)
- "Let's rewrite this in the new framework" (technology chase)
- "This isn't how I would have written it" (style preference)
- Right before a major release (wrong timing)
Bottom line: Refactor when it serves the business and the team, not when it serves your ego or aesthetic preferences. Always be able to articulate the specific problem you're solving.
This is the curse of success. Your system works, you add features, and gradually it becomes harder and harder to modify. Even systems designed with best practices end up fighting against new requirements.
Common causes of rigidity:
- Premature optimization: Locking in assumptions about performance or use cases
- Over-abstraction: Creating generic solutions for problems you don't actually have
- Under-abstraction: Duplicating logic that should be unified
- Poor boundaries: Modules that know too much about each other's internals
- Implementation-driven design: Structure based on how you built it, not what it needs to do
- Temporal coupling: Hidden dependencies on execution order
Yes - strategically chosen technical debt can be a smart business decision. The key word is strategically. Not all technical debt is created equal.
Martin Fowler's Technical Debt Quadrant:
- Deliberate & Prudent: "We must ship now and deal with consequences" ✅ Strategic
- Deliberate & Reckless: "We don't have time for design" ❌ Dangerous
- Inadvertent & Prudent: "Now we know how we should have done it" ✅ Learning
- Inadvertent & Reckless: "What's layering?" ❌ Disaster
When shortcuts make sense:
- Proving a business hypothesis before investing in perfect architecture
- Meeting a critical market window that won't exist in 3 months
- Temporary solution while designing the right long-term approach
- Isolated code that won't need to evolve much
Critical requirements for strategic debt:
- Awareness: You consciously recognize you're taking on debt
- Documentation: You record what the debt is and why it was taken
- Plan to repay: You have a concrete plan for when and how to address it
- Time-bounded: There's a specific timeline for paying it back
- Isolated impact: The shortcut doesn't spread to other parts of the system
What strong teams do differently:
According to Google's research, strong teams don't try to avoid tech debt altogether. They accept it strategically when it helps, but they pay it down with a plan. They also measure: "How often do you feel that incurring technical debt was the right decision?"
The danger zone:
The real problem is inadvertent debt - when you don't even realize you're creating problems for the future. This comes from lack of knowledge or skill, not conscious trade-offs. Preventing inadvertent debt requires learning proper design principles and techniques.
Bottom line: Don't obsess over perfect code. But do obsess over understanding the trade-offs you're making and having a plan to manage them.