# When Does Simplicity Become Too Simple? Finding the Threshold for Abstraction

> Discover the threshold where simplicity becomes too simple. Learn when to introduce abstraction to avoid maintenance headaches caused by repetition, changing needs, or testing issues.

- Repository: [Jiayuan Zhang/andrej-karpathy-skills](https://github.com/forrestchang/andrej-karpathy-skills)
- Tags: deep-dive
- Published: 2026-04-08

---

**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`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/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`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/skills/karpathy-guidelines/SKILL.md) at line 28 and repeated in [`CLAUDE.md`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/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:

```python
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`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/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:

```python
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:

```python
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`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/README.md), [`SKILL.md`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/SKILL.md), and [`CLAUDE.md`](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/CLAUDE.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.