
Kiss Principle
- 220 installs
- 22 repo stars
- Updated February 19, 2026
- markpitt/claude-skills
When designs, refactors, or feature specs grow over-engineered and you need Claude to bias toward the simplest solution that still meets requirements.
About
Encodes the Keep It Simple, Stupid principle so Claude favors minimal designs, fewer abstractions, and straightforward implementations over clever complexity during build-time planning and coding.
- Fights over-engineering during design and refactor
- Applies across frontend, backend, and API surfaces
- Pairs well with code review and scope trimming
- Encourages minimal viable abstractions
- Reinforces readable maintainable defaults
Kiss Principle by the numbers
- 220 all-time installs (skills.sh)
- +3 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #325 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/markpitt/claude-skills --skill kiss-principleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 220 |
|---|---|
| repo stars | ★ 22 |
| Last updated | February 19, 2026 |
| Repository | markpitt/claude-skills ↗ |
What it does
When designs, refactors, or feature specs grow over-engineered and you need Claude to bias toward the simplest solution that still meets requirements.
Files
K.I.S.S Principle Orchestration Skill
This skill helps you apply the K.I.S.S principle—"Keep It Simple, Stupid"—to systematically reduce unnecessary complexity while maintaining functionality and effectiveness. Simplicity is not about removing necessary features; it's about eliminating unnecessary complications.
Quick Reference: When to Load Which Resource
| Your Situation | Load Resource | Why |
|---|---|---|
| Need to understand KISS fundamentals and why simplicity matters | resources/kiss-fundamentals.md | Learn core concepts, history, and empirical benefits |
| Reviewing code, design, or system for unnecessary complexity | resources/complexity-analysis.md | Identify complexity sources, anti-patterns, red flags |
| Want proven strategies to simplify solutions | resources/simplification-strategies.md | Learn 10+ actionable techniques with software/UX examples |
| Building or designing something new with KISS in mind | resources/kiss-driven-design.md | Apply KISS during design, architecture, feature planning |
| Comparing simple vs complex approaches | resources/decision-frameworks.md | Use frameworks to evaluate tradeoffs, make smart choices |
| Seeing worked examples of effective simplification | resources/case-studies.md | Real cases: code refactoring, API design, UX improvements |
Core Principle
Simplicity is the ultimate sophistication. — Leonardo da Vinci
The K.I.S.S principle states that most systems work better when kept simple rather than made complex. Unnecessary complexity:
- Increases bugs and maintenance burden
- Makes systems harder to understand and modify
- Reduces performance and reliability
- Wastes development time and resources
- Creates cognitive overload for users and developers
The Goal: Solve problems with the minimum necessary complexity while preserving correctness and user value.
Orchestration Protocol
Phase 1: Assess Your Situation
Quickly identify what you're simplifying and why:
Context Type:
- Code Review/Refactoring: Existing code has unnecessary complexity → Load complexity-analysis.md
- Feature Design: Planning new functionality → Load kiss-driven-design.md
- Architecture/System Design: Building infrastructure or large systems → Load simplification-strategies.md
- Problem-Solving: Finding solution to technical challenge → Load decision-frameworks.md
- UX/Documentation: Improving clarity and usability → Load simplification-strategies.md
- Learning: Understanding KISS principles → Load kiss-fundamentals.md
Complexity Level:
- Light: Minor improvements, small scope → Use simplification-strategies.md directly
- Medium: Significant redesign needed, moderate scope → Use complexity-analysis.md + decision-frameworks.md
- Heavy: Major architectural changes, system-wide complexity → Use all resources systematically
Action: Load appropriate resource file(s) based on situation.
Phase 2: Analyze and Plan
Based on context, follow these steps:
| Step | What to Do | Resource |
|---|---|---|
| 1. Identify complexity | What makes this complex? What's unnecessary? | complexity-analysis.md |
| 2. Understand tradeoffs | What are we gaining/losing with each approach? | decision-frameworks.md |
| 3. Select strategies | Which simplification techniques apply here? | simplification-strategies.md |
| 4. Design simply | How would we design this with KISS in mind? | kiss-driven-design.md |
| 5. Verify value | Does the simple solution meet requirements? | decision-frameworks.md |
| 6. Reference examples | How have others solved this simply? | case-studies.md |
Phase 3: Execution & Validation
Before Simplifying:
- Preserve core functionality and requirements
- Document why current complexity exists
- Identify what stakeholders actually need vs. want
- Create rollback plan if needed
During Simplification: 1. Remove one layer of complexity at a time 2. Verify functionality after each change 3. Measure improvement (LOC, cyclomatic complexity, performance) 4. Document decisions and rationale
After Simplification:
- Test thoroughly (more bugs often hide in complexity)
- Gather feedback from team and users
- Monitor performance and stability
- Document simpler approach for future reference
Complexity Assessment Framework
Quickly evaluate if something is too complex:
Red Flags - This is Too Complex:
- ❌ You can't explain it in 2-3 sentences
- ❌ It requires extensive documentation to understand
- ❌ New team members struggle to modify it for weeks
- ❌ It has many interdependencies and side effects
- ❌ Performance problems correlate with feature addition
- ❌ Bugs consistently appear in this component
- ❌ It has deeply nested conditionals (>3 levels)
- ❌ Multiple abstractions on top of each other
- ❌ Over-engineered for current and foreseeable needs
Green Flags - This is Appropriately Simple:
- ✅ You can explain it clearly in 1-2 minutes
- ✅ New developers understand it quickly
- ✅ It does one thing well (single responsibility)
- ✅ Dependencies are explicit and minimal
- ✅ It's stable with few bugs
- ✅ Code is readable and self-documenting
- ✅ It serves current needs without speculation
KISS vs Over-Engineering
| Aspect | KISS | Over-Engineering |
|---|---|---|
| Scope | Solves current problem | Anticipates future needs |
| Code | ~100-200 LOC | ~500+ LOC |
| Time to Ship | 1-2 weeks | 4-8+ weeks |
| Maintenance | Easy to modify | Complex to change |
| Performance | Good enough | Highly optimized |
| Bugs | Few, obvious | Many, hidden |
| Approach | Add complexity when needed | Remove complexity as possible |
Key Principles to Remember
1. Necessity Test: Does every component, line, and feature serve a current user need? 2. Clarity First: Clear code beats clever code 3. Single Responsibility: Each function/module does one thing well 4. Minimal Dependencies: Fewer connections = fewer failure points 5. Explicit Better Than Implicit: Code should be obvious, not magical 6. Measure Before Optimizing: Don't optimize prematurely 7. Refactor Incrementally: Simplify gradually, test continuously
Common Complexity Anti-Patterns
See resources/complexity-analysis.md for detailed analysis of:
- Over-abstraction (too many layers)
- Premature optimization (optimizing before profiling)
- Gold plating (adding nice-to-haves)
- Speculative generalization (over-generalizing)
- Feature creep (scope expansion)
- Accidental complexity vs essential complexity
- Technical debt accumulation
Simplification Strategies
See resources/simplification-strategies.md for 10+ proven techniques:
- Constraint-based design
- Default assumptions
- Removing features
- Consolidating logic
- Flattening architecture
- Eliminating abstractions
- Standardizing approaches
- And more...
Resource Files Summary
resources/kiss-fundamentals.md
Foundation and philosophy:
- KISS principle definition and history
- Why simplicity matters (empirical evidence)
- Simplicity vs complexity tradeoffs
- Principles of effective simplification
- Common misconceptions
resources/complexity-analysis.md
Identifying and understanding complexity:
- Complexity sources and types
- Measuring complexity (cyclomatic, LOC, coupling)
- Identifying unnecessary complexity
- Recognizing over-engineering
- Red flags and anti-patterns
resources/simplification-strategies.md
Actionable techniques for simplifying:
- 10+ proven simplification strategies
- When to apply each strategy
- Code examples in multiple languages
- UX simplification approaches
- Architecture simplification patterns
resources/kiss-driven-design.md
Applying KISS from the start:
- Designing for simplicity
- Requirements gathering with KISS in mind
- Architecture patterns that promote simplicity
- Feature design principles
- Documentation and communication
resources/decision-frameworks.md
Making trade-off decisions:
- Simple vs Complex evaluation framework
- When complexity is justified
- Cost-benefit analysis for features
- Decision trees for architectural choices
- Measuring ROI of simplification
resources/case-studies.md
Real-world examples of successful simplification:
- Code refactoring case studies
- API design simplifications
- Architecture improvements
- UX improvements through simplification
- Decision-making examples
How This Skill Works
1. Assess your situation: What are you simplifying and why? 2. Analyze complexity: Identify unnecessary complication 3. Select approach: Choose relevant strategies and frameworks 4. Plan simplification: Design the simpler solution 5. Execute carefully: Make changes incrementally, test continuously 6. Validate: Confirm the simple solution meets all needs 7. Learn: Reference cases and patterns for future decisions
Quick Start: 5-Minute Simplification Check
1. Can you explain this in 2 sentences? (If no → too complex) 2. What would the simplest possible version look like? 3. What complexity is essential? What's optional? 4. Could you remove one component without breaking functionality? 5. What would new developers struggle with?
Templates & Checklists
- Complexity Assessment Checklist in
resources/complexity-analysis.md - Simplification Planning Template in
resources/simplification-strategies.md - Decision Framework Template in
resources/decision-frameworks.md - Design Checklist in
resources/kiss-driven-design.md
Common Scenarios
Scenario 1: Code Review → Load complexity-analysis.md → identify issues → Load simplification-strategies.md → suggest improvements
Scenario 2: Feature Design → Load kiss-fundamentals.md → Load kiss-driven-design.md → plan with simplicity in mind
Scenario 3: Architecture Redesign → Load complexity-analysis.md → assess current state → Load decision-frameworks.md → evaluate tradeoffs → Load simplification-strategies.md → plan improvements
Scenario 4: Learning from Examples → Load case-studies.md → study approaches → Load decision-frameworks.md → understand tradeoffs
Next Steps
1. Identify what you're working on (code, design, system, decision) 2. Load appropriate resource from table above 3. Assess complexity and identify problem areas 4. Select simplification strategies or design approaches 5. Plan and execute changes incrementally 6. Validate that simple version meets all requirements 7. Document decisions for team learning
---
Remember: The goal is solving the problem correctly with minimum necessary complexity. Simplicity requires discipline—resist the urge to over-engineer, and refactor ruthlessly to remove unnecessary complication.
"Everything should be as simple as it is, but not simpler." — Albert Einstein
Case Studies: Real-World Examples of Successful Simplification
This resource provides concrete examples of how organizations applied the K.I.S.S principle.
Case Study 1: Stripe's API Design
The Challenge: Build a payment processing API that developers love to use.
The Simplicity Approach:
- Standard RESTful API with simple endpoints
- One representation format (JSON)
- Consistent error handling
- Clear documentation
POST /charges
{
"amount": 2000,
"currency": "usd",
"source": "tok_visa"
}Result: Billions in transactions processed, became industry standard
Key Principle: Simple API beats powerful but complex API
---
Case Study 2: GitHub: Monolith That Scaled
The Challenge: Build a Git hosting platform that serves millions.
The Simplicity Approach:
- Monolithic Rails application initially
- Multiple servers for horizontal scaling
- Database optimization before splitting
- Only split services when necessary
Result: Shipped faster, team of 4 built million-user product
Key Principle: Start simple (monolith), scale when hitting real limits
---
Case Study 3: Basecamp: Feature Trimming
The Challenge: Compete with complex project management software.
The Simplicity Approach:
- 10 core features instead of 200
- Self-explanatory interface
- No extensive configuration
- Simple enough to learn in 30 minutes
Result: Simple product became extremely popular
Key Principle: 80% of users use 20% of features; build for the core
---
Case Study 4: Code Refactoring: Tangled Discount Logic
Before (Complex - 30+ lines):
- Deeply nested conditionals
- Business logic unclear
- Hard to test and modify
After (Simple - 15 lines):
- Clear configuration-based rules
- Business logic separated from code
- Easy to modify without code changes
- Testable in isolation
Key Principle: Configuration often simpler than conditional logic
---
Case Study 5: Slack's Data Model
The Challenge: Store and query millions of messages efficiently.
The Simplicity Approach:
- Single messages table
- PostgreSQL JSONB for flexible attributes
- Fewer joins, simpler queries
- Modern database features eliminate schema complexity
Result: Simpler queries, easier to maintain, scales better
Key Principle: Use modern database features instead of complex schema
---
Case Study 6: AWS Lambda
The Challenge: Reduce complexity of running code.
The Simplicity Approach:
def handler(event, context):
return {"statusCode": 200, "body": "Hello"}- Just write a function
- Scaling: automatic
- Cost: per-invocation
- Infrastructure: abstracted away
Result: Developers focus on code, not infrastructure
Key Principle: Abstracting away unnecessary complexity enables simplicity
---
Case Study 7: Vue.js vs React
The Challenge: Build reactive UI framework.
Vue's Simplicity Approach:
- Template syntax familiar to HTML developers
- State management integrated
- Less boilerplate
- Gentler learning curve
Result: Gained popularity despite React's head start
Key Principle: Familiar syntax reduces complexity
---
Case Study 8: Linux: Monolithic + Modular
The Challenge: Build OS that's both simple and extensible.
The Simplicity Approach:
- Simple core (process, memory, filesystem, networking)
- Loadable modules for specialization
- Doesn't force features into core
- POSIX standard reduces surprise
Result: Billions of devices run Linux, core remains simple
Key Principle: Simple core + modular extensions
---
Case Study 9: Markdown Format
The Challenge: Create document format for everyone.
The Simplicity Approach:
- Readable even as plain text
- Simple rules (headers, lists, emphasis)
- Extensible with HTML when needed
- No training required
Result: Most popular documentation format
Key Principle: Simplicity + elegance creates lasting products
---
Case Study 10: Getting Things Done (GTD)
The Challenge: Manage tasks without being overwhelmed.
The Simplicity Approach: 1. Capture everything 2. Process inbox 3. Organize into simple lists 4. Weekly review 5. Do one thing at a time
Result: Survived 20+ years, millions adopted it
Key Principle: Simple systems beat sophisticated ones if they work
---
Common Lessons
1. Start Simple, Evolve When Needed
- Add complexity only when hitting real limits
- Proven approach: Stripe, GitHub, AWS
2. Use Configuration Over Code
- Business logic in config files vs code
- Easier to change, easier to understand
3. Eliminate Unnecessary Options
- Remove features nobody uses
- Focus on core value
4. Modern Tools Enable Simplicity
- PostgreSQL JSONB: schema flexibility
- Lambda: infrastructure abstraction
- Modern frameworks: less boilerplate
5. Use Familiar Conventions
- HTML-like syntax in Vue
- Standard HTTP in REST APIs
- Plain text style in Markdown
6. Simple Core + Extension Points
- Linux kernel + loadable modules
- Stripe API + webhooks
---
How to Apply These Lessons
1. Identify complex area in your project 2. Find similar case study 3. Understand why simple approach worked 4. Adapt approach to your context 5. Implement incrementally 6. Measure improvement
Complexity Analysis: Identifying and Understanding Unnecessary Complexity
What is Complexity?
Complexity is the measure of how difficult a system is to understand, maintain, and modify.
Unnecessary complexity is complication that doesn't serve a current requirement or solve a real problem.
Understanding complexity sources is the first step toward simplification.
Types of Complexity
1. Structural Complexity
The structure and interconnections of components.
Indicators:
- Deep nesting of conditionals, loops, or function calls
- Multiple levels of abstraction or indirection
- Tangled dependencies between modules
- Unclear separation of concerns
Example (code with structural complexity):
# Complex: Multiple levels of indirection
class UserPermissionValidator:
def validate(self, user):
return self.permission_service.get_rules().evaluate(
self.context_builder.build_from(user).with_roles(
self.role_fetcher.fetch_for(user)
)
)
# Simple: Direct approach
def can_access(user, resource):
return user.role in ALLOWED_ROLES[resource]2. Cognitive Complexity
How much mental effort is required to understand what's happening.
Indicators:
- Code that doesn't read like English
- Business logic mixed with technical implementation
- Non-obvious control flow
- Implicit dependencies or behaviors
Example:
# Complex: Hard to follow logic
result = [x for x in (y[z[0]][z[1]] for z in coords if z)
if x and x.status == 'active']
# Simple: Clear what's happening
active_items = []
for coord in coords:
if coord:
item = data[coord[0]][coord[1]]
if item and item.status == 'active':
active_items.append(item)3. Operational Complexity
The runtime complexity of execution.
Indicators:
- Slow performance correlated with data size (O(n²), O(n³))
- Unnecessary database queries in loops
- Inefficient algorithms
- Memory leaks or bloat
Example:
# Complex: O(n²) nested loop
for user in users:
for order in orders:
if user.id == order.user_id:
process(order)
# Simple: O(n) with indexing
user_orders = {}
for order in orders:
user_orders.setdefault(order.user_id, []).append(order)
for user in users:
for order in user_orders.get(user.id, []):
process(order)4. Accidental Complexity
Complexity introduced by implementation choices, not by the problem itself.
Sources:
- Over-abstraction (too many interfaces/base classes)
- Speculative generalization (building for use cases that don't exist)
- Framework overhead (using enterprise framework for simple task)
- Premature optimization
- Unnecessary patterns (Strategy pattern for 2 options, Visitor pattern for simple tree walk)
Example:
# Accidental Complexity: Strategy pattern for simple case
class PaymentStrategy:
def pay(self, amount): pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
return charge_card(amount)
class CashPayment(PaymentStrategy):
def pay(self, amount):
return register_cash(amount)
payment = CreditCardPayment()
payment.pay(amount)
# Simple: Just call the right function
pay_by_card(amount) # or pay_by_cash(amount)5. Essential Complexity
Complexity inherent to the problem domain.
Examples:
- Distributed system coordination
- Compliance with regulatory requirements
- Complex business rules that mirror real-world complexity
- Mathematical algorithms for difficult problems
Characteristic: You cannot eliminate it without changing the problem itself.
Complexity Metrics
Cyclomatic Complexity
Measures the number of linearly independent paths through code.
How to Calculate:
- Count decision points (if, while, case, &&, ||, ?:)
- Add 1 for the base path
- CC = 1 + number of decision points
Interpretation:
- 1-3: Simple (good)
- 4-7: Moderate (acceptable, but consider simplifying)
- 8-10: High (should simplify)
- 11+: Very High (definitely needs refactoring)
Example:
def process_order(order): # CC = 1
if order.total > 100: # CC = 2
if order.customer_is_vip: # CC = 3
apply_discount(0.2)
else: # CC = 3 (branch)
apply_discount(0.1)
if order.is_rush: # CC = 4
expedite_shipping()
return order
# Total CC = 4Lines of Code (LOC)
Measures code volume.
Guidelines:
- Average function: 20-40 lines
- Average class: 200-400 lines
- If exceeding these, likely doing too much
Observation: More LOC doesn't always mean more functionality; it might mean more complexity.
Coupling
Measures dependencies between modules.
High Coupling Indicators:
- Changes to one component require changes to many others
- Difficult to test in isolation
- Modules are hard to reuse
- Changes ripple through system
Low Coupling Indicators:
- Components can be modified independently
- Easy to test in isolation
- Components are reusable
- Changes are localized
Depth of Nesting
Measures how deeply conditionals or loops are nested.
Guidelines:
- Depth 1-2: Good
- Depth 3: Acceptable but consider flattening
- Depth 4+: Should definitely refactor
Example of high nesting:
for item in items:
if item.valid:
for variant in item.variants:
if variant.available:
for pricing in variant.pricing:
if pricing.region == region:
# 4 levels deep!
process(pricing)Number of Parameters
Functions taking many parameters suggest they're doing too much.
Guidelines:
- 0-2 parameters: Good
- 3-4 parameters: Acceptable
- 5+ parameters: Refactor (likely too many responsibilities)
Identifying Unnecessary Complexity: Red Flags
Code Red Flags
🚩 Inconsistent naming: Variables named x, tmp, data1
- Indicates unclear purpose
- Problem-domain names are more readable
🚩 Comments explaining what code does (not why)
- Indicates code isn't self-documenting
- Solution: Refactor code to be clearer
🚩 Deep nesting (>3 levels)
- Indicates overly complex control flow
- Solution: Extract to separate functions, use early returns
🚩 Long functions (>50 lines consistently)
- Indicates multiple responsibilities
- Solution: Break into focused, single-purpose functions
🚩 Many parameters (>4)
- Indicates function doing too much
- Solution: Group related parameters into objects, simplify
🚩 Boolean trap (function has different behavior based on boolean parameter)
- Indicates multiple behaviors merged into one function
- Solution: Split into separate functions
# Boolean trap - hard to understand from call site
process(order, True) # What does True mean?
# Clear - separate functions
process_expedited(order)
process_standard(order)🚩 Layers of indirection (delegation to delegation)
- Indicates unnecessary abstraction
- Solution: Inline unnecessary abstractions
🚩 Magic numbers or strings
- Indicates unclear intent
- Solution: Use named constants
🚩 Highly coupled modules (changes everywhere when one thing changes)
- Indicates poor separation of concerns
- Solution: Reduce dependencies
Design Red Flags
🚩 Over-generalization: Building for use cases that don't exist yet
- "We might need this to handle..."
- "In the future, we could support..."
- Solution: YAGNI (You Aren't Gonna Need It) principle
🚩 Premature optimization: Optimizing before profiling
- Complex algorithms for uncommon cases
- Cache layers that aren't needed yet
- Solution: Optimize only what measurements show is slow
🚩 Feature accumulation: Adding features without removing old ones
- Code paths for deprecated features still present
- Feature flags everywhere
- Solution: Ruthlessly remove unused features
🚩 Speculative architecture: Building for scale you don't need yet
- Distributed system when one server works fine
- Microservices when monolith is appropriate
- Solution: Start simple, evolve as needed
🚩 Pattern overuse: Using sophisticated patterns everywhere
- Strategy pattern for 2 options
- Visitor pattern for simple tree walk
- Decorators for basic functionality
- Solution: Use patterns only when they reduce overall complexity
Organizational Red Flags
🚩 Knowledge silos: Only one person understands component
- Indicates unnecessary complexity
- Solution: Simplify until multiple people can understand it
🚩 Onboarding struggles: New developers struggle for weeks
- Indicates complexity is hidden or poorly understood
- Solution: Simplify or improve documentation
🚩 Frequent bugs in specific areas: Particular components break repeatedly
- Indicates complexity that's hard to reason about
- Solution: Refactor to simpler design
🚩 Fear of changing code: "Don't touch that, it'll break everything"
- Indicates tight coupling and hidden complexity
- Solution: Refactor to reduce dependencies
Anti-Patterns: Common Unnecessary Complexity
1. Over-Abstraction
Creating unnecessary layers of abstraction.
Problem:
# Unnecessary abstraction layers
class ConfigLoader:
def load(self): return ConfigParser().parse(self.file)
class ConfigService:
def __init__(self):
self.loader = ConfigLoader()
def get_config(self):
return self.loader.load()
# Actual usage
config = ConfigService().get_config()Solution:
# Direct approach
config = parse_config_file(config_file)2. Premature Optimization
Optimizing before identifying performance problems.
Problem:
# Complex caching for uncommon case
class CachedUserRepository:
def __init__(self):
self.cache = {}
self.lock = threading.RLock()
def get_user(self, id):
if id not in self.cache:
with self.lock:
if id not in self.cache: # Double-check lock
self.cache[id] = self._fetch(id)
return self.cache[id]Solution (if profiling shows need):
# Simple approach first
def get_user(id):
return database.query("SELECT * FROM users WHERE id = ?", id)3. Gold Plating
Adding "nice-to-have" features that aren't requirements.
Problem:
- Feature not requested by users
- Not in original requirements
- Adds complexity without clear value
- "Might be useful someday"
Solution: Build the minimum viable product, add features only when explicitly requested.
4. Speculative Generalization
Over-generalizing to support use cases that don't exist.
Problem:
# Over-generalized for uses that don't exist
class PaymentProcessor:
def process(self, payment_type, amount, options):
# 200 lines handling every possible payment type
if payment_type == 'credit_card':
# ...complex logic...
elif payment_type == 'bank_transfer':
# ...complex logic...
# ... 10 more payment typesSolution:
# Simple, specific solution
def process_credit_card_payment(amount, card_token):
return stripe_api.charge(amount, card_token)
def process_bank_transfer(amount, account):
return bank_api.transfer(amount, account)5. God Objects/Functions
Single component doing too many things.
Problem:
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self): ...
def apply_taxes(self): ...
def apply_discounts(self): ...
def validate_items(self): ...
def check_inventory(self): ...
def process_payment(self): ... # Too many responsibilities!
def send_confirmation_email(self): ...
def update_analytics(self): ...Solution: Break into focused classes/functions:
Order(data container)PricingCalculator(calculations)PaymentProcessor(payment)OrderNotifier(notifications)
Measuring Complexity in Your Codebase
Quick Assessment
1. Readability Test: Can a developer new to the project explain what this code does in 2 minutes?
- Yes → Appropriately complex
- No → Too complex
2. Change Test: How hard is it to add a new feature or fix a bug?
- Easy (localized change) → Appropriately complex
- Hard (changes needed everywhere) → Over-coupled, too complex
3. Knowledge Test: How many people fully understand this component?
- 3+ people → Good, not overly complex
- 1 person → Too complex or poorly documented
4. Onboarding Test: How long until new developer can safely modify this?
- < 1 week → Appropriately complex
- 2+ weeks → Too complex
Formal Metrics
Using tools:
- Python: pylint, radon (cyclomatic complexity)
- JavaScript: eslint-plugin-complexity
- Java: SonarQube
- C#: Roslyn analyzers
- IDE built-ins: Most IDEs show complexity metrics
What to look for:
- Functions with CC > 10 (refactor candidates)
- Classes with > 400 LOC (possibly too many responsibilities)
- Multiple parameters (> 4) on functions
- Deep nesting (> 3 levels)
Complexity Refactoring Checklist
When you identify unnecessary complexity:
- [ ] Understand it first: Why was this complexity added? (Look at git history, comments)
- [ ] Verify it's unnecessary: Does functionality depend on this complexity? (Extract and test)
- [ ] Plan refactoring: What's the simpler approach? (Sketch it out)
- [ ] Refactor incrementally: Small, testable changes not massive rewrite
- [ ] Test thoroughly: Ensure behavior is preserved
- [ ] Measure improvement: Track complexity metrics before/after
- [ ] Document decision: Why was it simplified? (For future context)
- [ ] Share learning: Teach team about pattern (prevent recurrence)
Common Complexity Patterns by Domain
Frontend/UI Complexity
- Over-componentization (components too small to be useful)
- State management over-engineering
- Unnecessary animation/visual complexity
- Component prop drilling (too many levels of prop passing)
Backend/API Complexity
- Over-abstraction of data access
- Unnecessary request/response validation layers
- Over-engineered error handling
- Middleware/interceptor chains
Database Complexity
- Over-normalization (too many tables, complex joins)
- Unnecessary caching layers
- Premature partitioning/sharding
- Complex migration strategies
Configuration Complexity
- Configuration too fine-grained (thousands of options)
- Environment-specific complexity
- Feature flag explosion
- Unnecessary parameter passing
Next Steps
1. Identify: Find the most complex parts of your codebase (use metrics or manual review) 2. Classify: Is this complexity essential or accidental? 3. Plan: What's the simpler approach? 4. Refactor: Use strategies from simplification-strategies.md 5. Validate: Test that behavior is preserved 6. Measure: Document improvement in metrics 7. Learn: Share what you learned with team
K.I.S.S-Driven Design: Applying Simplicity from the Start
This resource focuses on applying the K.I.S.S principle during design and architecture phases, not just refactoring.
Design Philosophy: Simplicity First
Before discussing specific design approaches, establish the right philosophy:
Principle 1: Start Simple, Add Complexity Only When Needed
- Default to simple solutions
- Prove need before adding complexity
- Better to refactor simple into complex than vice versa
- Simple is easier to change than complex
Principle 2: Constraints Enable Simplicity
- Unlimited scope leads to complexity
- Explicit constraints force better designs
- "We can only use one database"
- "Response must be < 100ms"
- "Deploy in < 1 hour"
- Constraints push toward elegant solutions
Principle 3: Understand the Problem First
- Don't design until you understand what you're solving
- Many "complex" problems are actually simple when understood
- Ask lots of questions before designing
- "What's the actual constraint?"
- "Who specifically will use this?"
- "What happens if we don't do this?"
Principle 4: Test Assumptions
- Many design decisions are based on guesses
- "We might need to scale to 1M users" (assumption)
- "Users will want X feature" (assumption)
- Design for current reality, not assumed futures
- Validate before building
Design Process for Simplicity
Phase 1: Problem Definition
Before any design, define the problem clearly.
Essential Questions: 1. What is the actual problem? (Not symptoms, the real problem) 2. Why does this problem exist? (What created the need?) 3. Who specifically needs this solved? (Real users, not "anyone could use it") 4. What does "solved" look like? (Measurable success criteria) 5. What constraints exist? (Budget, time, technical, regulatory) 6. What are we NOT solving? (Scope boundaries)
Example Problem Definition (Good vs Poor):
❌ Poor: "We need a user management system"
- Too vague
- No constraints
- Could be 10 different things
✅ Good: "We need a system for admins to manage 50-200 internal employees. Admins need to: add users, assign roles (admin/user), disable inactive users. Users just need to log in with email/password. Required: works for 200 users, deploy in 2 weeks, costs < $500/month."
- Specific scope
- Clear constraints
- Measurable success criteria
Phase 2: Requirements Gathering
Gather requirements focusing on necessity, not possibility.
Filtering Process:
User says: "It would be nice if users could manage their own profile pictures"
Questions:
1. Is this required for core functionality? NO
2. Do 80%+ of users actually do this? UNKNOWN
3. How much effort? 10+ hours for image processing/storage
4. What if we don't build it? Users can still use app
Recommendation: Don't build, revisit in v2 if users requestRequirement Evaluation Framework:
| Requirement | Must Have | Should Have | Nice to Have |
|---|---|---|---|
| User login | ✓ | ||
| User roles | ✓ | ||
| Email notifications | ✓ | ||
| SMS notifications | ✓ | ||
| User profiles | ✓ | ||
| Profile pictures | ✓ | ||
| Two-factor auth | ✓ |
Design for "Must Have" and "Should Have". Build "Nice to Have" only if time/resources allow.
Phase 3: Constraint-Based Architecture
Design architecture around constraints, not aspirations.
Constraints That Drive Simple Design:
Time Constraint:
"Deploy in 2 weeks" forces simple design
- No time for elegant abstractions
- Choose proven technologies over new ones
- Minimal custom code
- Use libraries/frameworks heavily
"Can take 6 months" allows more care, but don't use for over-engineeringScale Constraint:
"Support 100 users initially" (not 1M)
- Simple database is fine
- No need for sharding/caching
- Monolithic architecture fine
- Scale when you hit the limit
"Must support 1M requests/day from day 1"
- Design for scale
- Cache and database complexity justified
- Distributed systems appropriateTechnology Constraint:
"Must use Java for integration with legacy system"
- Constrains technology stack
- Simplifies decision-making
- But stay simple within Java ecosystem
"Must deploy on customer servers"
- Affects architecture (containerization, deployment model)
- Justifies certain complexity (configuration management)Cost Constraint:
"Cannot exceed $100/month in cloud costs"
- Simple database, no data warehousing
- No expensive tools
- Monolith instead of microservices
- Justifies code complexity to reduce infrastructurePhase 4: Simple Patterns Over Complex Frameworks
Choose design patterns and frameworks based on fit, not sophistication.
Pattern Selection:
❌ Don't: Choose sophisticated patterns because they're elegant
# Over-engineered: Using Strategy pattern for 2 options
class PaymentStrategy: pass
class CardPayment(PaymentStrategy): pass
class CashPayment(PaymentStrategy): pass
payment = PaymentStrategy.create(payment_type)
payment.pay(amount)✅ Do: Choose patterns that simplify your actual problem
# Simple: Just call the right function
if payment_type == 'card':
pay_by_card(amount)
elif payment_type == 'cash':
pay_by_cash(amount)
# Or even simpler with a dictionary:
payment_handlers = {
'card': pay_by_card,
'cash': pay_by_cash
}
payment_handlers[payment_type](amount)Framework Selection:
❌ Don't: "This full-featured enterprise framework will handle anything"
- Overkill for your needs
- Lots of configuration
- Heavy learning curve
- Performance overhead
✅ Do: Choose framework that fits your actual scope
Simple API: Flask (Python) or Express (Node)
Not: Django or Spring for simple API
Simple UI: Plain HTML/CSS/JS or Svelte
Not: Full Redux/Redux-Saga/Immutable ecosystem for simple app
Data processing: Script or simple library
Not: Full Spark cluster for processing 10GB dataSimple Architecture Patterns
Pattern 1: Monolithic First
Start monolithic, split if needed. Most systems don't need to split.
Benefits:
- Single codebase to understand
- Simple deployment
- Easy testing
- Clear dependencies (circular imports become obvious)
- Straightforward debugging
When to Split: Only when you hit specific limits:
- Deployment frequency (can't deploy fast enough)
- Team scaling (too many developers conflicting)
- Scaling different components differently
- Technology requirements (different languages optimal)
Good monolith structure:
/app
/auth <- Isolated module
/users <- Isolated module
/orders <- Isolated module
/payments <- Isolated module
/main.py <- All use same database
Result: Simple, but clear separation. Easy to split later if needed.Pattern 2: Simple Data Store
Use single database technology unless proven otherwise.
❌ Complex: PostgreSQL + MongoDB + Redis + Elasticsearch + S3
- Multiple technologies to learn/manage
- Data consistency complexity
- Deployment complexity
- Monitoring/ops complexity
- Each tool adds overhead
✅ Simple: PostgreSQL for everything
- One technology to master
- ACID guarantees
- JSONB for semi-structured data
- Full-text search capability
- Can add Redis if profiling shows need
PostgreSQL usage:
- Users table (structured data)
- Orders table (structured data)
- User settings (JSONB column)
- Full-text search (built-in)
- Caching (see if needed first)
Only split to MongoDB if:
- Profiling shows JSON is bottleneck
- Truly unstructured data is 50%+ of workloadPattern 3: Simple Deployment
Deploying should be simple. If it's complex, simplify.
❌ Complex: Kubernetes with service mesh and 10 environment variables
- Steep learning curve
- Lots of configuration
- Fragile deployments
- Hard to debug
- Appropriate for: Large teams, complex multi-service systems✅ Simple: Docker container on single server
- Learn Docker (1-2 days)
- Deploy: docker pull && docker run
- Backup: rsync database
- Debug: docker logs
- Appropriate for: Small to medium systemsSimple Deployment Checklist:
- [ ] Deployment is one command
- [ ] Deployment takes < 5 minutes
- [ ] Rollback is simple and fast
- [ ] Logs are accessible and clear
- [ ] One person can deploy
- [ ] Deployment happens regularly (multiple times per week)
Pattern 4: Simple Configuration
Configuration should be minimal and clear.
❌ Complex: 100 environment variables, 10 config files, complex precedence rules
- Hard to understand current state
- Easy to misconfigure
- Ops burdens✅ Simple: Core config in single file, environment overrides
# config.json
{
"database_url": "postgres://localhost/myapp",
"port": 3000,
"log_level": "info"
}
# Overridable by environment variables
DATABASE_URL=prod.db PORT=8000 ./app.pySimple Configuration Principles:
- Default sensible values (don't require configuration for normal use)
- Environment variables for per-deployment config
- Minimal number of options
- Clear names that indicate purpose
- Fail fast if invalid
Pattern 5: Simple API Design
API should be easy to understand and use.
❌ Complex: Hypermedia, content negotiation, many query parameters
GET /api/v1/users?include=profile,posts&filter[name]=John&sort=-created_at&page[number]=2&page[size]=50&fields[users]=name,email✅ Simple: Obvious endpoints, standard behavior
GET /users -> List all users
GET /users/123 -> Get user 123
POST /users -> Create user
PUT /users/123 -> Update user
DELETE /users/123 -> Delete user
GET /users?name=John -> Filter by name (standard)Simple API Principles:
- RESTful conventions (GET, POST, PUT, DELETE)
- Standard HTTP status codes (200, 400, 401, 404, 500)
- Standard request/response format (JSON)
- Minimal headers
- Obvious endpoint structure
- Standard error format
Pattern 6: Simple Error Handling
Errors should be clear and actionable.
❌ Complex: Many custom exception types, context objects, logging at every level
throw new UserServiceException(
"Failed to update user",
ErrorCode.USER_UPDATE_FAILED,
originalException,
buildContext(user, operation)
)✅ Simple: Clear, consistent error handling
def update_user(user_id, data):
if not user_exists(user_id):
return {"error": "User not found"}, 404
try:
user = save_user(user_id, data)
return {"user": user}, 200
except ValidationError as e:
return {"error": str(e)}, 400
except Exception as e:
logger.error(f"Unexpected error updating user: {e}")
return {"error": "Internal error"}, 500Design Checklist for Simplicity
Before finalizing a design, use this checklist:
Scope & Requirements:
- [ ] Requirements clearly defined
- [ ] Scope explicitly limited
- [ ] "Must have" vs "nice to have" prioritized
- [ ] Success criteria measurable
- [ ] Constraints documented
Architecture:
- [ ] Architecture can be explained in 5 minutes
- [ ] Major components clearly identified
- [ ] Dependencies documented
- [ ] One person can understand full architecture
- [ ] Deployment model is simple
Technology Choices:
- [ ] Technologies chosen for fit, not coolness
- [ ] Each technology justified
- [ ] Team understands chosen technologies
- [ ] Fewer than 3 data stores (unless justified)
- [ ] Fewer than 5 services (start monolithic)
Complexity Analysis:
- [ ] No "just in case" features
- [ ] No "might be useful" abstractions
- [ ] No speculative optimization
- [ ] All design decisions documented
- [ ] Simpler alternatives considered and rejected
Operability:
- [ ] Deployment is one command
- [ ] Configuration is simple
- [ ] Monitoring is straightforward
- [ ] Debugging is clear
- [ ] Rollback is simple
Testing & Validation:
- [ ] Design is testable
- [ ] Test approach is straightforward
- [ ] No heavy mocking required
- [ ] Key assumptions documented and testable
- [ ] Critical paths have clear test strategy
Anti-Patterns to Avoid in Design
Anti-Pattern 1: Future-Proofing Everything
"Let's design it to handle 1M users and 100 payment methods"
Problem: Over-engineered for current needs
Better: "Design for current + one iteration, refactor when hitting limits"Anti-Pattern 2: Abstracting Too Early
"Let's create an abstraction interface for everything"
Problem: Abstractions that don't reflect real use cases, coupling through abstraction
Better: Implement concrete, refactor to abstraction when patterns emergeAnti-Pattern 3: Choosing Technology for Resume Value
"Let's use Kafka/Kubernetes/gRPC because it's cool"
Problem: Tool-centric instead of problem-centric; adds complexity unnecessarily
Better: Choose the simplest technology that solves the problemAnti-Pattern 4: Adding Features Nobody Asked For
"Users will probably want multi-language support"
Problem: Complexity without validation; users might not care
Better: Ask users, validate need, build if requestedAnti-Pattern 5: Premature Optimization
"We should cache everything in Redis"
Problem: Complexity without measurement; might not be bottleneck
Better: Build simple, measure, optimize measured bottlenecksDesign Review Questions
When reviewing a design, ask these questions:
1. Clarity: Can you explain this design in 5 minutes? (If no, too complex) 2. Necessity: Is every component necessary for v1? (If no, remove) 3. Fit: Does chosen technology fit the constraints? (If no, simplify) 4. Understanding: Could new team member understand this? (If no, simplify) 5. Testing: Is the design easy to test? (If no, simplify) 6. Operation: Is deployment and operation simple? (If no, simplify) 7. Assumptions: Are any assumptions unvalidated? (If yes, validate first) 8. Simpler Alternative: Is there a simpler way? (Discuss)
Common Design Scenarios
Scenario 1: "Should We Use Microservices?"
❌ Complex from start:
- Microservices and Kubernetes
- Service mesh
- Message queues
- Complex deployment
✅ Simple path: 1. Start with monolith (fast to ship) 2. If hitting limits → add async queue 3. If team growing → split service 4. If scaling issues → add caching/optimization 5. Only if truly needed → Kubernetes
Scenario 2: "How Should We Handle Payments?"
❌ Complex:
- Custom payment processing
- Multiple payment gateways
- Custom fraud detection
✅ Simple:
- Use Stripe API (handles 99% of cases)
- Use their SDK (proven, tested)
- Use their fraud detection (included)
- Deploy in < 1 hour
Scenario 3: "What About Search?"
❌ Complex:
- Build Elasticsearch cluster
- Separate search index
- Complex sync logic
✅ Simple:
- Start with database full-text search
- Move to Elasticsearch only if bottleneck
- Proven you need dedicated search before building
Next Steps for Design Work
1. Define problem clearly using questions from Phase 1 2. Gather requirements with focus on "must have" 3. Identify constraints (time, scale, cost, technology) 4. Choose simple patterns that fit constraints 5. Use proven technologies appropriate to scope 6. Document why each choice (decision log) 7. Review for simplicity using checklist 8. Build and measure before adding complexity
K.I.S.S Principle Fundamentals
Definition and History
K.I.S.S stands for "Keep It Simple, Stupid" (or sometimes "Keep It Short and Simple").
The principle originated in the U.S. Navy in the 1960s, attributed to Kelly Johnson, lead engineer at Lockheed's Skunk Works division. Johnson observed that systems designed with simplicity in mind were more effective, reliable, and maintainable than over-engineered alternatives. The basic idea: "Most systems work better if they are kept simple rather than made complex."
The principle has since become fundamental to software engineering, product design, user experience, and general problem-solving across disciplines.
Core Concept
Simplicity is not the absence of features—it's the absence of unnecessary complexity.
A system can have all needed features and still be simple. A system can have few features but be unnecessarily complex. The goal is:
- ✅ Include all features needed to solve the problem correctly
- ✅ Eliminate unnecessary abstraction and indirection
- ✅ Make the solution understandable to those who need to work with it
- ✅ Reduce maintenance burden and failure points
Why Simplicity Matters: Evidence
Development Efficiency
- Faster to build: Simple solutions take less time to design, code, and test
- Fewer bugs: Complexity correlates strongly with bug count (research shows ~3x more defects in complex code)
- Easier to debug: When bugs occur, simple code is easier to trace
- Faster shipping: Get to working software sooner
Maintenance and Evolution
- Easier to understand: New team members ramp up faster
- Safer to modify: Fewer side effects and dependencies to worry about
- Cheaper to maintain: Less code to review, test, and support
- Better longevity: Simple systems age better than complex ones
Quality and Reliability
- More testable: Simple code is easier to write comprehensive tests for
- Fewer failure points: Every component adds potential failure modes
- Better performance: Simpler code often performs better (fewer layers, indirection)
- Clearer requirements: Simple designs force clarity about what's actually needed
Team Dynamics
- Reduced cognitive load: Team can hold entire system in mind
- Better communication: Simple designs are easier to explain and discuss
- Fewer arguments: Clear, simple solutions have fewer edge cases to debate
- Knowledge transfer: New people learn faster
User Experience
- Easier to learn: Simpler products are more intuitive
- Faster performance: Simpler code often runs faster
- Fewer bugs affecting users: Simpler systems are more reliable
- Clear value proposition: Simplicity makes features obvious to users
Essential vs Accidental Complexity
Understanding the distinction is crucial to applying KISS well.
Essential Complexity
Complexity that is inherent to the problem domain and cannot be eliminated without changing the problem:
Examples:
- Complex business logic that mirrors real-world complexity
- Multi-step algorithms required by problem requirements
- Distributed system coordination inherent to the problem
- Necessary security measures
- Required compliance or regulatory requirements
Approach: Don't try to eliminate essential complexity. Instead:
- Document it well
- Isolate it from other parts of system
- Manage it carefully but don't remove it
- Consider if problem can be decomposed differently
Accidental Complexity
Complexity introduced by implementation choices that is not required by the problem:
Examples:
- Over-engineered abstractions
- Premature optimization
- Speculative generalization ("we might need this someday")
- Multiple layers of indirection
- Unnecessary frameworks or libraries
- Complex build/deployment pipelines
- Tangled dependencies
Approach: Eliminate ruthlessly. This is where KISS applies most powerfully.
Simplicity Principles
1. Necessity Test
Every component, function, line of code, and feature should pass this test:
- Does this serve a current, documented requirement?
- Would removing it break functionality that users/stakeholders need?
- If you can't explain why it's there, it's probably unnecessary
2. Clarity First
- Clear code beats clever code every time
- Self-documenting code is better than code requiring comments
- Explicit is better than implicit
- Readable beats concise when you have to choose
3. Single Responsibility
- Each module, class, function should have one reason to change
- Do one thing well, don't try to be general-purpose unless needed
- Clear boundaries make systems easier to reason about
4. Minimal Dependencies
- Every dependency is a potential failure point
- Fewer connections = fewer ripple effects
- Explicit dependencies are better than implicit ones
- Dependency management is a key part of simplicity
5. Constraint-Based Thinking
- Constraints often force simpler, more elegant solutions
- "How would we do this with 50% the code?"
- "How would we do this with 1 database table instead of 5?"
- Constraints push you past obvious/complex solutions
6. Refactor Relentlessly
- First version doesn't need to be simple—second version does
- As you learn what you actually need, simplify ruthlessly
- Technical debt compounds; pay it down regularly
- Simplification is ongoing, not one-time activity
Common Misconceptions
Misconception 1: "KISS means no features"
Reality: KISS means all necessary features, no unnecessary ones.
- A feature users need is not "unnecessary complexity"
- A framework that enables 10 needed features isn't over-engineering
- The question is: are we adding this feature because it's needed or because it might be useful someday?
Misconception 2: "KISS means no abstraction"
Reality: KISS means abstraction only where it adds clarity.
- Good abstractions reduce complexity (they hide unnecessary details)
- Bad abstractions add complexity (they create indirection without benefit)
- Ask: does this abstraction make the system easier or harder to understand?
Misconception 3: "KISS means quick and dirty"
Reality: KISS means clean, well-thought-out solutions with minimal parts.
- Quick and dirty accumulates technical debt
- Simple solutions are still thoroughly tested and well-designed
- The difference: simple solutions don't have unnecessary layers
Misconception 4: "Simple solutions are less powerful"
Reality: Simple solutions are often more powerful.
- Simpler code is often faster (fewer layers to traverse)
- Simple designs are easier to extend when needs change
- Complex systems are rigid; simple systems are flexible
Misconception 5: "KISS only applies to code"
Reality: KISS applies everywhere.
- UI/UX design (fewer options, clearer interfaces)
- Process design (fewer steps, clearer workflows)
- Architecture (fewer components, clearer responsibilities)
- Specifications (fewer edge cases, clearer requirements)
- Documentation (clearer structure, essential info only)
KISS vs Over-Engineering: Decision Matrix
| Factor | KISS Approach | Over-Engineering |
|---|---|---|
| Scope | Current requirements + 1 foreseeable iteration | Anticipates many possible futures |
| Code Volume | Minimal, focused | Extensive, speculative |
| Abstraction Layers | 1-2 where beneficial | 4+ layers of abstraction |
| Framework/Library Dependencies | Minimal, well-justified | Many, "just in case" |
| Time to Deliver | Faster (weeks) | Slower (months) |
| Initial Flexibility | High (simple to modify) | Appears high (abstraction) but often rigid |
| Maintenance Burden | Low (clear, focused code) | High (many parts to maintain) |
| Team Ramp-up | Fast (clear to understand) | Slow (complex to understand) |
| Bug Count | Lower (less code to break) | Higher (more complexity) |
| Performance | Often better (fewer layers) | Often worse (abstraction overhead) |
| When Needs Change | Easy to adapt (simple base) | Difficult (rigid structure) |
The Irony: Over-engineered systems with "flexible abstractions" often become rigid when real needs diverge from anticipated ones. Simple systems are more flexible because you can adapt them more easily.
When Complexity IS Justified
KISS doesn't mean "always choose simple." Sometimes complexity is justified:
Justified Complexity Scenarios
1. Essential Complexity: The problem inherently requires it
- Distributed systems need coordination complexity
- Security requires cryptographic complexity
- Compliance requires control complexity
2. Scale Requirements: Complexity required to meet non-functional needs
- Performance optimization for massive scale
- Reliability requirements for critical systems
- Caching/indexing for data access patterns
3. Integration Realities: Complexity required to work with existing systems
- Legacy system integration
- Regulatory system requirements
- Third-party API constraints
4. Team Capability: Complexity appropriate to team's expertise
- A framework might be justified if team knows it well
- Patterns appropriate to team's experience level
- Technology choices that team can maintain
Testing Justified Complexity
Before accepting complexity: 1. Can we eliminate it? Try hard—most complexity can be removed 2. Why is it here? Document the reason explicitly 3. Is it measured? How do we know we needed it? (Measure first) 4. Is it isolated? Can we contain it so rest of system stays simple? 5. Can we explain it? If team can't explain it, it's probably not justified
Applying KISS in Practice
For Individuals
1. Ask questions: "Do we really need this? Could it be simpler?" 2. Refactor regularly: Find simplifications in existing code 3. Resist perfection: Shipping simple is better than perfect-but-not-shipped 4. Learn patterns: Simple approaches that work in your domain 5. Document tradeoffs: When you add complexity, explain why
For Teams
1. Value simplicity: Make it a core code review criterion 2. Refactor together: Pair programming for simplification 3. Share knowledge: Document the simple approaches that work 4. Resist scope creep: Keep saying "no" to unnecessary features 5. Measure it: Track complexity metrics over time
For Organizations
1. Tooling: Invest in simple, integrated tools not complex suites 2. Processes: Simpler processes beat complex ones 3. Architecture: Simple architectures scale better than complex ones 4. Culture: Reward shipping simple solutions, not impressive technical depth 5. Training: Help teams develop intuition for simplicity
Key Metrics for Simplicity
Track these to measure if your system is appropriately simple:
- Cyclomatic Complexity: Lower is better (target: < 5 per function)
- Lines of Code: Proportional to functionality (higher LOC/feature ratio means over-engineering)
- Time to Onboard: How long before new developer can make changes safely?
- Bug Density: Bugs per line of code (higher complexity → more bugs)
- Average Function/Class Size: Larger often means doing too much (target: ~30 lines avg)
- Dependency Count: Fewer dependencies = simpler system
- Test Coverage Ratio: Simpler code is easier to test thoroughly
- Time to Fix Bugs: How long to find and fix? (simple systems are faster)
Quotes on Simplicity
"The most important principle for the good design of experiments is to have absolute clarity of purpose." — Ronald A. Fisher
"Simplicity is the ultimate sophistication." — Leonardo da Vinci
"Any intelligent fool can make things bigger and more complex. It takes a touch of genius—and a lot of courage—to move in the opposite direction." — E.F. Schumacher
"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away." — Antoine de Saint-Exupéry
"The code that's hardest to delete is the code that's hardest to write. Don't write it." — Rich Hickey
"Simplicity and elegance are unpopular because they require hard work and discipline to achieve and education to be appreciated." — Edsger Dijkstra
Next Steps
- Review
complexity-analysis.mdto identify unnecessary complexity in your current work - Study
simplification-strategies.mdto learn techniques for eliminating it - Check
case-studies.mdfor real examples of successful simplification - Use
decision-frameworks.mdwhen deciding between simple and complex approaches
Simplification Strategies: Proven Techniques for Reducing Complexity
This resource provides 10+ actionable strategies for eliminating unnecessary complexity from code, designs, and systems.
Strategy 1: Constraint-Based Design
The Idea: Artificially constrain your solution to force simpler approaches.
How It Works:
- "Design this with 50% fewer lines of code"
- "Build this with only one database table"
- "Create this with no if statements"
- Constraints force creative, simpler solutions
When to Use:
- During initial design
- When you're stuck in complexity
- When reviewing over-engineered solutions
Example: Database Schema
Complex approach: 10 normalized tables with complex joins
├── users
├── user_profiles
├── user_permissions
├── roles
├── role_permissions
├── accounts
├── account_users
├── subscriptions
├── billing_history
└── invoices
Constraint: "Use only 3 tables"
Simple approach:
├── accounts (id, name, created_at)
├── users (id, account_id, name, email, role, permissions_json, created_at)
└── billing (id, account_id, status, amount, created_at)Example: Function Complexity
# Complex: 45 lines, nested conditionals
def calculate_price(order, user, region):
base_price = sum(item.price for item in order.items)
if user.is_vip:
discount = 0.2
elif user.is_repeat:
discount = 0.1
else:
discount = 0.0
if order.total > 100 and user.is_vip:
discount = min(discount + 0.1, 0.5)
# ... more complex logic
return final_price
# Constraint: "What if all discounts were a single lookup?"
def calculate_price(order, user, region):
base_price = sum(item.price for item in order.items)
discount = DISCOUNT_MATRIX[user.tier][order.total > 100]
return base_price * (1 - discount)
# DISCOUNT_MATRIX = {
# 'vip': {True: 0.3, False: 0.2},
# 'repeat': {True: 0.15, False: 0.1},
# 'new': {True: 0.05, False: 0.0}
# }Strategy 2: Remove Features/Options
The Idea: Many features add complexity without proportional value. Remove them.
How It Works: 1. List all features/options 2. Identify which are rarely used 3. Remove the least-used ones 4. Measure impact
When to Use:
- When feature count grows without proportional benefit
- During API design
- In configuration systems
- In UI design
Example: HTTP Library
Complex: Supporting 20 different authentication methods
Simple: Support only 3 (API key, OAuth2, JWT)
Complex: Supporting 15 HTTP status codes
Simple: Support only 4 (200, 400, 401, 500)
Complex: 50 configuration options
Simple: 5 essential options, rest have sensible defaultsExample: Configuration System
Before:
{
"logging.level": "debug",
"logging.format": "json",
"logging.output": "file",
"logging.file_path": "/var/log/app.log",
"logging.max_size": 10485760,
"logging.max_backups": 5,
"logging.compress": true,
"logging.retention_days": 30,
// ... 50 more options
}
After:
{
"log_level": "debug", // debug, info, warn, error
"output": "stdout" // stdout or file
}Strategy 3: Consolidate Duplicate Logic
The Idea: When similar logic appears multiple times, consolidate into one place.
How It Works: 1. Find duplicated patterns/logic 2. Extract to shared function/class 3. Call from multiple places 4. Maintain single source of truth
When to Use:
- When code is copy-pasted
- When similar patterns appear in multiple places
- During code review
Example:
# Before: Duplicated validation everywhere
def create_user(name, email):
if not name or len(name) < 2:
raise ValueError("Invalid name")
if not email or "@" not in email:
raise ValueError("Invalid email")
# ... create user
def update_user(user_id, name, email):
if name and (not name or len(name) < 2):
raise ValueError("Invalid name")
if email and (not email or "@" not in email):
raise ValueError("Invalid email")
# ... update user
# After: Consolidated validation
def validate_user_input(name=None, email=None):
if name is not None and (not name or len(name) < 2):
raise ValueError("Invalid name")
if email is not None and (not email or "@" not in email):
raise ValueError("Invalid email")
def create_user(name, email):
validate_user_input(name, email)
# ... create user
def update_user(user_id, name=None, email=None):
validate_user_input(name, email)
# ... update userStrategy 4: Use Defaults and Conventions
The Idea: Instead of requiring explicit configuration, use sensible defaults.
How It Works: 1. Identify most common usage pattern 2. Make that the default behavior 3. Allow override only when needed 4. Reduces need for configuration
When to Use:
- In API design
- In frameworks and libraries
- In configuration systems
- In UI design
Example: API Design
Complex: Require all parameters
GET /users?page=1&per_page=20&sort_by=name&sort_order=asc&include=profile&include=posts
Simple: Sensible defaults
GET /users # Uses page=1, per_page=20, sort by name asc
GET /users?page=2 # Only override what you need
GET /users?per_page=50&sort_by=created_at
# API defaults:
# - page: 1
# - per_page: 20
# - sort_by: created_at
# - sort_order: descExample: Configuration
Complex: Specify everything
server:
host: 0.0.0.0
port: 3000
timeout: 30000
max_connections: 100
keepalive: true
compression: true
Simple: Specify only what's different from defaults
server:
port: 3000Strategy 5: Flatten Architecture
The Idea: Reduce number of abstraction layers.
How It Works: 1. Identify layers of indirection 2. Remove unnecessary layers 3. Go directly from client to implementation 4. Maintain clear separation where it matters
When to Use:
- When reviewing architecture
- When tracing through code reveals unnecessary indirection
- During refactoring
Example: Unnecessary Layers
Complex (5 layers):
UI → ViewController → Service → Repository → ORM → Database
Simple (3 layers):
UI → Service → Database
Complex (4 layers):
HTTP Request → Router → Controller → BusinessService → DataService
Simple (2 layers):
HTTP Request → HandlerExample: Dependency Injection
# Complex: Overly abstracted
class UserFactory:
def __init__(self, config, logger):
self.config = config
self.logger = logger
def create_repository(self):
return UserRepositoryImpl(
DatabaseConnection(self.config.db),
self.logger
)
class UserService:
def __init__(self, factory):
self.repository = factory.create_repository()
# Simple: Direct dependencies
class UserService:
def __init__(self, db_connection):
self.db = db_connection
# Usage is just as clear and less indirectionStrategy 6: Extract Single-Purpose Functions
The Idea: When a function does multiple things, split it into focused functions.
How It Works: 1. Identify multiple responsibilities in a function 2. Extract each responsibility to separate function 3. Call from original location or refactor callers 4. Improves reusability and testability
When to Use:
- When functions exceed 30-40 lines
- When functions have multiple reasons to change
- When testing requires setting up complex state
Example:
# Before: Multiple responsibilities
def process_order(order):
# Validation
if not order.items:
raise ValueError("Empty order")
# Calculation
total = sum(item.price * item.quantity for item in order.items)
tax = total * 0.1
# Persistence
db.save_order(order)
# Notification
send_email(order.customer, f"Order placed: ${total + tax}")
# Analytics
track_event('order_created', {'amount': total, 'items': len(order.items)})
return total + tax
# After: Single-purpose functions
def validate_order(order):
if not order.items:
raise ValueError("Empty order")
def calculate_total(order):
subtotal = sum(item.price * item.quantity for item in order.items)
return subtotal * 1.1 # Including 10% tax
def place_order(order):
validate_order(order)
total = calculate_total(order)
db.save_order(order)
send_order_confirmation(order, total)
track_order_event(order, total)
return totalStrategy 7: Use Simple Data Structures
The Idea: Complex data structures often indicate unclear thinking. Use simple structures (dicts, lists, tuples).
How It Works: 1. Instead of custom classes, use built-in types 2. Use dictionaries for key-value pairs 3. Use lists for collections 4. Create custom types only when it adds clarity
When to Use:
- When designing data structures
- When you find yourself writing getters/setters
- When data is just passed through multiple functions
Example:
# Complex: Custom classes for everything
class UserProfile:
def __init__(self, first_name, last_name, email):
self.first_name = first_name
self.last_name = last_name
self.email = email
def get_full_name(self):
return f"{self.first_name} {self.last_name}"
def get_display_name(self):
return self.first_name.upper()
# Simple: Use dictionaries
user = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'john@example.com'
}
full_name = f"{user['first_name']} {user['last_name']}"
display_name = user['first_name'].upper()Exception: When it provides clarity or domain-specific behavior, use custom types:
# Makes sense to create custom type
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def add(self, other):
if other.currency != self.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
# Simple dictionaries wouldn't make the constraints obviousStrategy 8: Remove Layers of Indirection
The Idea: Every level of indirection makes code harder to follow. Remove unnecessary ones.
How It Works: 1. Identify indirection (calling another function that just calls another) 2. Determine if the indirection adds value 3. If not, remove it (inline the intermediate function) 4. If yes, document why it's there
When to Use:
- During code review
- When tracing through code requires following multiple function calls
- When refactoring
Example:
# Unnecessarily indirect
def get_user(user_id):
return fetch_user(user_id)
def fetch_user(user_id):
return database.query("SELECT * FROM users WHERE id = ?", user_id)
# Direct and clearer
def get_user(user_id):
return database.query("SELECT * FROM users WHERE id = ?", user_id)
# If the indirection had a purpose, keep it
def get_user(user_id):
"""Get user with permission check"""
if not current_user.can_access(user_id):
raise PermissionError()
return database.query("SELECT * FROM users WHERE id = ?", user_id)Strategy 9: Use Configuration Over Codification
The Idea: Behaviors defined in configuration are simpler than behaviors in code.
How It Works: 1. Identify behaviors that differ based on context (environment, user type, etc.) 2. Move to configuration instead of if/else branches 3. Load configuration at startup 4. Use configuration values in logic
When to Use:
- When you have many if/else branches checking context
- When behavior differs between environments
- When behavior might change without code changes
Example:
# Complex: Business logic in code
def calculate_shipping(order, region):
if region == 'US':
if order.weight < 5:
return 5.99
elif order.weight < 10:
return 8.99
else:
return 12.99
elif region == 'EU':
if order.weight < 5:
return 8.99
# ... more logic
elif region == 'ASIA':
# ... more logic
# Simple: Business logic in configuration
SHIPPING_RATES = {
'US': {
(0, 5): 5.99,
(5, 10): 8.99,
(10, float('inf')): 12.99
},
'EU': {
(0, 5): 8.99,
(5, 10): 12.99,
(10, float('inf')): 15.99
}
}
def calculate_shipping(order, region):
rates = SHIPPING_RATES[region]
for (min_weight, max_weight), rate in rates.items():
if min_weight <= order.weight < max_weight:
return rateStrategy 10: Standardize Approaches
The Idea: When teams use different approaches for similar problems, pick one and standardize.
How It Works: 1. Identify where teams use different patterns for similar problems 2. Evaluate alternatives 3. Choose the simplest that works 4. Enforce through code review and standards 5. Refactor existing code to standard
When to Use:
- When growing teams have divergent practices
- When similar functionality is implemented differently
- During architectural decisions
Example:
Problem: How do we handle errors in our API?
Current: Multiple approaches mixed together
- Some endpoints return { error: "message" }
- Some return { errors: ["message1", "message2"] }
- Some return { status: "error", message: "..." }
- Some return HTTP status with plain text body
Simplified standard: Consistent error format
{
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
}
}Strategy 11: Default to Library/Framework Features
The Idea: Before building custom solutions, use what your framework provides.
How It Works: 1. Know your framework's capabilities 2. Before custom code, check if framework has it 3. Use built-in features where possible 4. Only build custom when framework doesn't apply
When to Use:
- When building infrastructure features
- When designing common patterns
- During architecture decisions
Example:
Instead of building custom:
- Logging → Use established logging library
- Error handling → Use framework's error handling
- Configuration → Use framework's config system
- Authentication → Use established auth library
- Caching → Use standard cache libraries
- Testing → Use framework's test utilitiesStrategy 12: Incremental Generalization
The Idea: Don't generalize prematurely. Build specific solutions, generalize only when patterns emerge.
How It Works: 1. Build specific solution for current problem 2. When similar problem arises, build second specific solution 3. When third similar problem appears, generalize 4. Refactor the three implementations to common pattern
When to Use:
- When designing reusable components
- When building frameworks or libraries
- During feature development
Example:
Wrong: Generalize immediately
// Guess at generalization before we know patterns
class Repository<T> {
public T get(string id) { ... }
public List<T> getAll() { ... }
public void save(T item) { ... }
public void delete(T item) { ... }
}
Right: Specific first, generalize when needed
// First: UserRepository (specific)
class UserRepository {
public User get(string id) { ... }
public void save(User user) { ... }
}
// Second: OrderRepository (specific)
class OrderRepository {
public Order get(string id) { ... }
public void save(Order order) { ... }
}
// Third: ProductRepository (specific)
// Now we see the pattern!
// Then: Extract common pattern
class Repository<T> {
public T get(string id) { ... }
public void save(T item) { ... }
}Simplification Planning Template
When facing complexity, use this template to plan simplification:
COMPLEXITY ANALYSIS
===================
What's complex?
- Component: ________________
- Measured complexity: ________ (CC, LOC, etc.)
- Why is it complex? ________
ESSENTIAL vs ACCIDENTAL
=======================
What complexity is essential (problem requires it)?
- _______________
- _______________
What complexity is accidental (implementation choice)?
- _______________
- _______________
STRATEGY SELECTION
==================
Which strategies apply? (check all that apply)
- [ ] Constraint-based design
- [ ] Remove features
- [ ] Consolidate duplicates
- [ ] Use defaults
- [ ] Flatten architecture
- [ ] Extract functions
- [ ] Simple data structures
- [ ] Remove indirection
- [ ] Use configuration
- [ ] Standardize approaches
- [ ] Use library features
- [ ] Incremental generalization
SIMPLE SOLUTION
===============
How would the simplest possible solution look?
- ________________________
What are we gaining?
- Reduced LOC: _______
- Reduced complexity: _____
- Faster onboarding: _____
What are we losing? (if anything)
- ________________________
EXECUTION PLAN
==============
Step 1: _______________
Step 2: _______________
Step 3: _______________
Verification:
- [ ] All tests pass
- [ ] Performance acceptable
- [ ] Team understands approach
- [ ] Documentation updatedKey Takeaways
1. Multiple Strategies: No single strategy works for everything. Know all 12 and choose appropriately.
2. Iterative: Simplification happens incrementally. Keep refactoring.
3. Measurement: Know before and after metrics to prove improvement.
4. Team Alignment: Ensure team understands why simplification is happening.
5. Prevention: Better to design simple than to simplify later. Use constraints and defaults from the start.
Next Steps
- Identify the most complex part of your system
- Assess using
complexity-analysis.md - Select 2-3 relevant strategies from this guide
- Make incremental changes and measure results
- Document what you learned for team