When Does Simplicity Become Too Simple? Finding the Threshold for Abstraction
Abstractions should only appear when they solve a real, recurring need—specifically when you encounter repeated manual work, changing requirements, or testability concerns that make the simple solution harder to maintain than a shared abstraction.
The Karpathy‑style guidelines from the forrestchang/andrej-karpathy-skills repository codify a "simple first" philosophy that prioritizes flat, readable code over premature architectural patterns. According to the repository's documentation, understanding exactly when plain code crosses the threshold into "too simple" is critical for maintaining velocity without accruing technical debt.
The Core Rule: No Abstractions for Single‑Use Code
The foundational guideline appears across multiple canonical files in the repository. In README.md at line 46, the directive is explicit: "No abstractions for single‑use code." This same rule is reinforced in skills/karpathy-guidelines/SKILL.md at line 28 and repeated in CLAUDE.md at line 46, creating a consistent policy across the project's documentation and tool integrations.
This principle serves as the default position. Until code demonstrates a concrete need for reuse or extension, it should remain as a flat, linear implementation without interfaces, inheritance, or dependency injection.
Three Signals That You've Crossed the Threshold
The repository identifies specific conditions that indicate the "simple" solution has become too simple and requires abstraction.
Repeated Manual Work (The Duplication Threshold)
When the same logic appears in more than two places, the maintenance cost of keeping those copies synchronized outweighs the complexity cost of a shared abstraction. The guideline suggests that duplication in > 2 locations is a reliable quantitative trigger for refactoring.
Changing Requirements (Conditional Explosion)
As requirements expand—for example, a discount calculator that must handle percentage, fixed amount, and tiered discounts simultaneously—the original single‑function implementation sprouts conditional branches. When if/else blocks begin obscuring the core business logic, this signals that the threshold for abstraction has been crossed.
Performance or Testability Concerns
Monolithic functions that mix I/O, validation, and business logic become difficult to unit test. When you cannot verify the pure business logic in isolation without mocking external systems, extracting that logic into a testable abstraction improves coverage without altering external behavior.
Code Examples: From Simple Function to Strategic Abstraction
The following progression demonstrates how to recognize and act upon the threshold for simplicity and abstraction.
The Simple Implementation (Single‑Use)
For a one‑off discount calculation, the repository recommends keeping the implementation minimal:
def calculate_discount(amount: float, percent: float) -> float:
"""Return a simple percentage discount."""
return amount * (percent / 100)
Why it remains unabstracted: This function is used in a single location, contains no branching logic, and is fully covered by a concise unit test. According to the guidelines in README.md, this requires no interface or strategy pattern.
When Requirements Expand (Strategy Pattern)
When the project requires three distinct discount modes—percentage, fixed amount, and tier‑based—the simple function would accumulate complex conditional logic. This is the threshold where abstraction becomes justified:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, amount: float) -> float:
"""Return the discount amount for *amount*."""
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent: float):
self.percent = percent
def calculate(self, amount: float) -> float:
return amount * (self.percent / 100)
class FixedDiscount(DiscountStrategy):
def __init__(self, fixed: float):
self.fixed = fixed
def calculate(self, amount: float) -> float:
return min(self.fixed, amount)
def apply_discount(amount: float, strategy: DiscountStrategy) -> float:
return amount - strategy.calculate(amount)
Why this abstraction is justified: The DiscountStrategy interface will be reused across multiple services (checkout, admin UI, promotional engine), eliminating duplicated conditional logic while making each strategy independently testable.
Verification Through Testing
Following the repository's goal‑driven execution philosophy, any abstraction must be verified with concrete tests that preserve original behavior:
def test_percentage_discount():
strat = PercentageDiscount(10) # 10 %
assert apply_discount(200, strat) == 180
def test_fixed_discount():
strat = FixedDiscount(30) # $30 max
assert apply_discount(50, strat) == 20
assert apply_discount(20, strat) == 0 # cannot go negative
These tests confirm that the new abstraction maintains existing functionality while adding extensibility for future discount types.
The Goal‑Driven Refactoring Process
The repository's philosophy emphasizes surgical changes rather than speculative architecture. Introducing an abstraction before you have a failing test or concrete need violates the development loop, creating unmaintained moving parts. When you do cross the threshold and decide to abstract, add only enough scaffolding—a small class, a protocol, or a helper function—to encapsulate the repeated logic. Avoid adding configurability, dependency injection frameworks, or extra interfaces until a concrete use‑case forces it.
Summary
- Default to simplicity: Keep code flat and linear until evidence contradicts this approach, as specified in
README.md,SKILL.md, andCLAUDE.md. - Watch for three signals: Duplication in more than two locations, expanding requirements that create conditional complexity, and untestable monolithic functions.
- Abstract surgically: When crossing the threshold, introduce only the minimal abstraction needed to solve the recurring problem—nothing more.
- Verify with tests: Ensure every abstraction preserves existing behavior through concrete test cases before finalizing the refactor.
Frequently Asked Questions
How many times should code be duplicated before abstracting?
According to the Karpathy‑style guidelines, duplication in more than two locations typically indicates that the maintenance overhead of synchronization outweighs the cost of abstraction. However, the decision also depends on the complexity of the duplicated logic—simple one‑liners may tolerate more duplication than complex algorithms.
What's the risk of abstracting too early?
Premature abstraction creates "additional moving parts that are not yet exercised," as noted in the repository's architectural reasoning. This leads to over‑engineered systems with unused flexibility, making the codebase harder to understand and modify when actual requirements emerge.
How do I know if my abstraction is too complex?
If the abstraction requires configuration, dependency injection, or interfaces that aren't strictly necessary for the current use‑cases, it has exceeded the threshold. The guidelines recommend "nothing more" than the minimal scaffolding needed to encapsulate repeated logic.
Does this apply to all programming languages?
Yes. While the examples use Python, the principle of "no abstractions for single‑use code" is language‑agnostic. Whether working in JavaScript, Go, Rust, or Java, the threshold for abstraction remains consistent: wait for concrete, recurring needs before introducing architectural patterns.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →