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
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
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
Instructions
When evaluating an abstraction:
- •Search for existing abstractions - Use Grep/Read to find similar patterns in codebase
- •Apply the evaluation checklist - Run through all 5 questions in section 4
- •Check for red flags - Scan code for patterns in section 5
- •Recommend simpler alternative - Provide concrete code example
- •Document decision - Explain why abstraction is/isn't needed
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
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.
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.
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: ...
Integration with Architecture Validation
This skill complements existing architecture validation skills:
Use with:
- •
architecture-validate-architecture- Check layer boundaries while avoiding unnecessary layers - •
architecture-validate-layer-boundaries- Ensure layers are necessary and well-justified - •
quality-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)
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
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.