
minimal-abstractions
by dawiddutoit
Collection of Claude Code skills, agents, and plugins
SKILL.md
name: minimal-abstractions description: | Enforces the principle of avoiding unnecessary abstractions and complexity in codebases. Use when evaluating new abstractions, reviewing architecture proposals, detecting over-engineering, or simplifying existing code. Triggers on "is this abstraction necessary", "too many layers", "simplify architecture", "reduce complexity", "over-engineered", "do we need this interface", or when reviewing design patterns. Works with architecture reviews, code reviews, and refactoring tasks. allowed-tools:
- Read
- Grep
- Bash
Minimal Abstractions
Quick Start
Before adding a new abstraction (interface, abstract class, wrapper, layer), ask:
- Does a similar abstraction already exist in this project?
- Are there 2+ concrete use cases requiring this abstraction RIGHT NOW?
- Is the complexity cost justified by actual flexibility needs?
If any answer is "no" or "maybe" → Don't add it. Use the simplest solution that works.
Table of Contents
- When to Use This Skill
- What This Skill Does
- Core Philosophy
- Abstraction Evaluation Checklist
- Detection Patterns (Red Flags)
- Examples: Good vs Over-Engineered
- Integration with Architecture Validation
- Expected Outcomes
- Red Flags to Avoid
1. When to Use This Skill
Explicit Triggers (User asks):
- "Is this abstraction necessary?"
- "Are we over-engineering this?"
- "Too many layers in this code"
- "Simplify this architecture"
- "Reduce complexity"
- "Do we need this interface/wrapper/layer?"
- "Should we use [design pattern]?"
Implicit Triggers (Autonomous invocation):
- Reviewing pull requests with new interfaces/abstract classes
- Architecture proposals introducing new layers
- Code reviews where new patterns are introduced
- Refactoring tasks aimed at simplification
- When a new abstraction is proposed and only one implementation exists
Debugging/Problem Triggers:
- Maintenance burden is high due to abstraction complexity
- Team members confused by excessive indirection
- Tests are difficult to write due to layering
- Simple changes require touching multiple abstraction layers
2. What This Skill Does
This skill provides a systematic framework for:
- Evaluating whether new abstractions are warranted
- Detecting over-engineering and unnecessary complexity
- Guiding developers to use existing project abstractions
- Preventing abstraction proliferation
- Simplifying code by removing unneeded layers
3. Core Philosophy
The Abstraction Principle
Abstractions are not free - every interface, wrapper, layer, or pattern adds:
- Cognitive load (developers must understand the abstraction)
- Maintenance burden (more code to test, debug, refactor)
- Indirection cost (harder to trace execution flow)
- Rigidity (abstractions create assumptions that resist change)
When abstractions ARE valuable:
- Multiple concrete implementations exist or are planned imminently
- Decoupling is critical (e.g., domain from infrastructure in Clean Architecture)
- Testability requires dependency injection
- Third-party integrations need isolation
When abstractions are NOT valuable:
- "We might need it someday" (YAGNI - You Aren't Gonna Need It)
- "It's a best practice" (without understanding why)
- Only one implementation exists and no others are planned
- Wrapping for wrapping's sake
Prefer Existing Abstractions
Before creating a new abstraction:
- Search the codebase for similar patterns
- Extend or adapt existing abstractions
- Reuse project-standard patterns (e.g., Repository, Service, Handler)
- Only create new abstractions when existing ones truly don't fit
4. Abstraction Evaluation Checklist
Use this checklist before adding ANY new abstraction (interface, abstract class, wrapper, layer):
Question 1: Does this abstraction already exist?
- Searched codebase for similar interfaces/abstractions
- Checked project architecture docs for standard patterns
- Reviewed existing layers (domain, application, infrastructure)
- Considered extending existing abstractions instead
Action: If similar abstraction exists → Use it. Don't create a new one.
Question 2: Do we have 2+ concrete implementations RIGHT NOW?
- Count current implementations (not hypothetical future ones)
- Verify implementations are actually different (not just copy-paste)
- Check if "multiple implementations" are really needed
Action: If <2 implementations → Skip the abstraction. Use concrete implementation directly.
Question 3: Is there a concrete business/technical requirement?
- Can you name the specific requirement driving this abstraction?
- Is the requirement current (not speculative)?
- Would removing this abstraction make the code harder to maintain?
Action: If requirement is speculative → Don't build it yet. Wait for actual need.
Question 4: What is the complexity cost?
- Count files touched to add this abstraction
- Estimate lines of code added (interface + implementations + tests)
- Consider cognitive load for new team members
- Evaluate impact on debugging and tracing
Action: If cost > benefit → Simplify. Use direct solution.
Question 5: Can we solve this with simpler patterns?
- Would a simple function work instead of an interface?
- Could dependency injection handle this without abstraction?
- Is this trying to solve a problem we don't have?
Action: If simpler solution exists → Use it.
5. Detection Patterns (Red Flags)
Red Flag 1: Lonely Interface
# ❌ Over-engineered: Interface with only one implementation
class DataProcessor(Protocol):
def process(self, data: dict) -> dict: ...
class JsonDataProcessor: # Only implementation
def process(self, data: dict) -> dict:
return transform_json(data)
Why it's a red flag: No actual polymorphism. The interface adds indirection without benefit.
Better approach:
# ✅ Simple: Direct implementation
def process_json_data(data: dict) -> dict:
return transform_json(data)
Red Flag 2: Wrapper with No Value
# ❌ Over-engineered: Wrapper that just forwards calls
class DatabaseWrapper:
def __init__(self, db: Database):
self._db = db
def query(self, sql: str) -> list:
return self._db.query(sql) # Just forwarding
Why it's a red flag: No transformation, validation, or added behavior. Pure indirection.
Better approach: Use the database directly or add actual value (caching, retry, validation).
Red Flag 3: Layer for Layer's Sake
# ❌ Over-engineered: Unnecessary service layer
class UserRepository: # Already exists
def get_user(self, id: int) -> User: ...
class UserService: # Adds nothing
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, id: int) -> User:
return self.repo.get_user(id) # Just forwarding
Why it's a red flag: Service layer adds no business logic, validation, or orchestration.
Better approach: Use repository directly until business logic is needed.
Red Flag 4: Pattern for Pattern's Sake
# ❌ Over-engineered: Factory for single type
class UserFactory:
@staticmethod
def create_user(name: str, email: str) -> User:
return User(name=name, email=email)
Why it's a red flag: Factory pattern used without variation or complexity justification.
Better approach:
# ✅ Simple: Direct construction
user = User(name="Alice", email="alice@example.com")
Red Flag 5: Premature Generalization
# ❌ Over-engineered: Generic solution for specific problem
class ConfigLoader(Generic[T]):
def load(self, source: str, parser: Parser[T]) -> T: ...
class JsonParser(Parser[dict]): ...
class YamlParser(Parser[dict]): ...
Why it's a red flag: Generic abstraction built before knowing actual requirements.
Better approach: Start with simple JSON config loader. Generalize when second format is needed.
6. Examples: Good vs Over-Engineered
Example 1: Repository Pattern
Over-Engineered:
# Unnecessary: Abstract repository + generic base + implementation
class Repository(Protocol, Generic[T]):
def get(self, id: int) -> T: ...
def save(self, entity: T) -> None: ...
class BaseRepository(Generic[T]): # Generic base
def validate(self, entity: T) -> bool: ...
class UserRepository(BaseRepository[User]): # Concrete
def get(self, id: int) -> User: ...
def save(self, user: User) -> None: ...
Right-Sized:
# Clean Architecture: Protocol in domain, implementation in infrastructure
# domain/repositories.py
class UserRepository(Protocol): # Interface for dependency inversion
def get_user(self, id: int) -> User: ...
def save_user(self, user: User) -> None: ...
# infrastructure/repositories.py
class SqlUserRepository: # Concrete implementation
def get_user(self, id: int) -> User: ...
def save_user(self, user: User) -> None: ...
Example 2: Service Layer
Over-Engineered:
# Unnecessary: Service that just forwards to repository
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, id: int) -> User:
return self.repo.get_user(id) # No business logic!
Right-Sized:
# Use repository directly until business logic emerges
class AuthenticationHandler:
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
def authenticate(self, email: str, password: str) -> Result[User, AuthError]:
user = self.user_repo.get_user_by_email(email)
if not user:
return Err(AuthError.USER_NOT_FOUND)
if not verify_password(password, user.password_hash):
return Err(AuthError.INVALID_PASSWORD)
return Ok(user)
Example 3: Reusing Existing Abstractions
Over-Engineered:
# Project already has Repository pattern
# Adding NEW abstraction for similar purpose:
class DataAccessLayer(Protocol): # Duplicates Repository!
def fetch(self, id: int) -> Entity: ...
def persist(self, entity: Entity) -> None: ...
Right-Sized:
# Use existing Repository pattern
class ProductRepository(Protocol): # Follows project convention
def get_product(self, id: int) -> Product: ...
def save_product(self, product: Product) -> None: ...
7. Integration with Architecture Validation
This skill complements existing architecture validation skills:
Use with:
architecture-validate-architecture- Check layer boundaries while avoiding unnecessary layersarchitecture-validate-layer-boundaries- Ensure layers are necessary and well-justifiedquality-code-review- Evaluate abstractions during PR review
Integration pattern:
- Run architecture validation to check existing patterns
- Use minimal-abstractions to evaluate NEW abstractions
- Ensure new code follows project patterns (don't reinvent)
8. Expected Outcomes
Successful Simplification
Before:
src/
├── domain/
│ ├── interfaces/user_repository.py
│ ├── interfaces/user_service.py
│ ├── interfaces/user_validator.py
├── application/
│ ├── services/user_service.py (forwards to repo)
│ ├── validators/user_validator.py (just calls validate())
├── infrastructure/
│ ├── repositories/user_repository.py
After (applying minimal-abstractions):
src/
├── domain/
│ ├── repositories.py (UserRepository protocol)
│ ├── models.py (User with validation)
├── application/
│ ├── handlers.py (CreateUserHandler with actual business logic)
├── infrastructure/
│ ├── repositories.py (SqlUserRepository)
Metrics:
- 40% fewer files
- 60% less indirection
- Same functionality
- Clearer execution paths
Validation Output
Abstraction Evaluation: ProductService
✅ Checklist Results:
❌ Does abstraction already exist? YES - Repository pattern exists
❌ 2+ implementations? NO - Only one service planned
❌ Concrete requirement? NO - "We might need microservices later"
⚠️ Complexity cost: +3 files, +200 LOC, +2 layers indirection
✅ Simpler solution exists? YES - Use repository + handler directly
Recommendation: SKIP THIS ABSTRACTION
- Use existing ProductRepository
- Add business logic to ProductHandler
- Wait for concrete multi-service requirement before abstracting
9. Red Flags to Avoid
Anti-Patterns
- ❌ "We might need it later" (YAGNI violation)
- ❌ Creating interfaces with only one implementation
- ❌ Wrappers that just forward calls without adding value
- ❌ Service layers that add no business logic
- ❌ Generic solutions for specific problems
- ❌ Design patterns used without understanding why
- ❌ Creating new abstractions when project patterns exist
Good Practices
- ✅ Use existing project abstractions first
- ✅ Wait for 2+ concrete implementations before abstracting
- ✅ Justify every layer with concrete requirements
- ✅ Prefer simple, direct solutions
- ✅ Question every new abstraction
- ✅ Measure complexity cost vs benefit
- ✅ Remove abstractions when they're no longer needed
Notes
Key Principle: Every abstraction must justify its existence with concrete, current requirements - not hypothetical future needs.
Balance: This skill advocates for minimal abstractions, but respects architectural patterns when they provide real value (e.g., Clean Architecture's dependency inversion).
When in doubt: Start simple. Add abstractions when pain points emerge, not before.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon