home / skills / flpbalada / my-opencode-config / code-architecture-wrong-abstraction

code-architecture-wrong-abstraction skill

/skills/code-architecture-wrong-abstraction

This skill guides when to duplicate versus abstract, helping you avoid wrong abstractions and wait for patterns to emerge.

npx playbooks add skill flpbalada/my-opencode-config --skill code-architecture-wrong-abstraction

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.8 KB
---
name: code-architecture-wrong-abstraction
description: Guides when to abstract vs duplicate code. Use this skill when creating shared utilities, deciding between DRY/WET approaches, or refactoring existing abstractions.
---

# Code Architecture: Avoiding Wrong Abstractions

## Core Principle

**Prefer duplication over the wrong abstraction. Wait for patterns to emerge before abstracting.**

Premature abstraction creates confusing, hard-to-maintain code. Duplication is far cheaper to fix than unwinding a wrong abstraction.

## The Rule of Three

Don't abstract until code appears in **at least 3 places**. This provides enough context to identify genuine patterns vs coincidental similarities.

```jsx
// ✅ Correct: Wait for the pattern to emerge
// First occurrence - just write it
const userTotal = items.reduce((sum, item) => sum + item.price, 0);

// Second occurrence - still duplicate
const cartTotal = products.reduce((sum, p) => sum + p.price, 0);

// Third occurrence - NOW consider abstraction
const calculateTotal = (items, priceKey = 'price') =>
  items.reduce((sum, item) => sum + item[priceKey], 0);
```

## When to Abstract

### ✅ Abstract When

- Same code appears in **3+ places**
- Pattern has **stabilized** (requirements are clear)
- Abstraction **simplifies** understanding
- Use cases share **identical behavior**, not just similar structure

### ❌ Don't Abstract When

- Code only appears in 1-2 places
- Requirements are still evolving
- Use cases need **different behaviors** (even if structure looks similar)
- Abstraction would require parameters/conditionals for variations

## The Wrong Abstraction Pattern

This is how wrong abstractions evolve:

```jsx
// 1️⃣ Developer A spots duplication and extracts it
function processData(data) {
  return data.map(transform).filter(validate);
}

// 2️⃣ New requirement is "almost" compatible
function processData(data, options = {}) {
  let result = data.map(options.customTransform || transform);
  if (options.skipValidation) return result;
  return result.filter(options.customValidate || validate);
}

// 3️⃣ More variations pile up...
function processData(data, options = {}) {
  let result = data;
  if (options.preProcess) result = options.preProcess(result);
  result = result.map(options.customTransform || transform);
  if (!options.skipValidation) {
    result = result.filter(options.customValidate || validate);
  }
  if (options.postProcess) result = options.postProcess(result);
  if (options.sort) result = result.sort(options.sortFn);
  return options.limit ? result.slice(0, options.limit) : result;
}

// ❌ Now it's incomprehensible spaghetti
```

## How to Fix Wrong Abstractions

The fastest way forward is **back**:

1. **Inline** the abstraction back into each caller
2. **Delete** the portions each caller doesn't need
3. **Accept** temporary duplication for clarity
4. **Re-extract** proper abstractions based on current understanding

```jsx
// Before: One bloated function trying to do everything
processData(users, { customTransform: formatUser, skipValidation: true });
processData(orders, { sort: true, sortFn: byDate, limit: 10 });

// After: Inline and simplify each use case
const formattedUsers = users.map(formatUser);
const recentOrders = orders.sort(byDate).slice(0, 10);

// Later: If true patterns emerge, abstract properly
```

## Hidden Costs of Abstraction

| Benefit | Hidden Cost |
|---------|-------------|
| Code reuse | **Accidental coupling** between unrelated modules |
| Single source of truth | **Layers of indirection** obscure bugs |
| DRY compliance | **Organizational inertia** makes refactoring painful |

## Facade Pattern: When It Becomes a Wrong Abstraction

Facades wrap complex subsystems behind a simple interface. They're useful but often become wrong abstractions when overused.

### The Typography Component Trap

```tsx
// ❌ Facade that becomes limiting
<Typography variant="body" size="sm">Hello</Typography>

// What if you need <small> or <mark>?
// Now you must extend the facade first:
<Typography variant="body" size="sm" as="small">Hello</Typography>  // Added prop
<Typography variant="body" size="sm" as="mark">Hello</Typography>   // Another prop

// ❌ Facade keeps growing with every edge case
type TypographyProps = {
  variant: 'h1' | 'h2' | 'body' | 'caption';
  size: 'sm' | 'md' | 'lg';
  as?: 'p' | 'span' | 'small' | 'mark' | 'strong' | 'em';  // Growing...
  weight?: 'normal' | 'bold';
  color?: 'primary' | 'secondary' | 'muted';
  // ... more props for every HTML text feature
};
```

### When Facade Works

```tsx
// ✅ Good: Facade encapsulates complex logic
<DatePicker
  value={date}
  onChange={setDate}
  minDate={today}
/>
// Hides: localization, calendar rendering, keyboard nav, accessibility

// ✅ Good: Facade enforces design system constraints
<Button variant="primary" size="md">Submit</Button>
// Ensures consistent styling, no arbitrary colors
```

### When to Skip the Facade

```tsx
// ✅ Sometimes native HTML is clearer
<small className="text-muted">Fine print</small>
<mark>Highlighted text</mark>

// vs forcing everything through a facade:
<Typography variant="small" highlight>...</Typography>  // ❌ Overengineered
```

### Facade Trade-offs

| Use Facade When | Skip Facade When |
|-----------------|------------------|
| Hiding **complex logic** (APIs, state) | Wrapping **simple HTML elements** |
| Enforcing **design constraints** | One-off styling needs |
| Team needs **consistent patterns** | Juniors need to learn the underlying tech |
| Behavior is **stable and well-defined** | Requirements are still evolving |

### The Junior Developer Test

If a junior must:
1. Learn the facade API
2. Then learn the underlying technology anyway
3. Then extend the facade for edge cases

...the facade adds friction, not value. Sometimes `ctrl+f` and manual updates across files is simpler than maintaining a leaky abstraction.

## Quick Reference

### DO

- Wait for **3+ occurrences** before abstracting
- Let patterns **emerge naturally**
- Optimize for **changeability**, not DRY compliance
- Test **concrete features**, not abstractions
- **Inline bad abstractions** and start fresh

### DON'T

- Abstract based on **structural similarity** alone
- Add parameters/conditionals to **force fit** new use cases
- Preserve abstractions due to **sunk cost fallacy**
- Fear **temporary duplication**

## Key Philosophies

| Approach | Meaning | When to Use |
|----------|---------|-------------|
| **DRY** | Don't Repeat Yourself | After patterns stabilize |
| **WET** | Write Everything Twice | Default starting point |
| **AHA** | Avoid Hasty Abstractions | Guiding principle |

## References

- [The Wrong Abstraction - Sandi Metz](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction)
- [The Wet Codebase - Dan Abramov](https://www.deconstructconf.com/2019/dan-abramov-the-wet-codebase)
- [AHA Programming - Kent C. Dodds](https://kentcdodds.com/blog/aha-programming)

Overview

This skill guides engineers on when to abstract code versus when to accept duplication. It emphasizes waiting for clear, recurring patterns before extracting shared utilities and shows how to detect and repair wrong abstractions. Use it to make pragmatic decisions that prioritize clarity and changeability over premature DRYing.

How this skill works

The skill inspects usage frequency, variation in behavior, and stability of requirements to decide if abstraction is warranted. It applies the Rule of Three: avoid extracting until similar code appears in at least three places. It also identifies signs of a wrong abstraction (growing option bags, conditional branching, and accidental coupling) and prescribes how to back out safely.

When to use it

  • Creating shared utilities from duplicated code paths
  • Deciding between DRY and intentional duplication during feature work
  • Refactoring a bloated helper or facade that gained many options
  • Designing UI wrappers (components/facades) versus using native elements
  • Evaluating whether evolving requirements justify a new abstraction

Best practices

  • Wait for 3+ real occurrences before extracting a general utility
  • Prefer small, focused abstractions that enforce a single responsibility
  • Inline and simplify callers before re-extracting if an abstraction becomes messy
  • Optimize for changeability and clarity, not for eliminating every duplicate line
  • Avoid large option bags; favor composing small functions instead

Example use cases

  • You see similar reduce/aggregate logic in three modules — extract a calculateTotal helper
  • A Typography facade keeps growing with props — inline specific cases and reconsider the facade
  • A processData function has many flags — inline behavior per caller and re-extract focused helpers
  • Building a DatePicker that hides complexity is fine; wrapping simple HTML tags with a facade is not
  • During fast-evolving requirements, duplicate code to keep each implementation clear and flexible

FAQ

What exactly is the Rule of Three?

Don't abstract until the same code exists in at least three places; that frequency helps confirm a stable pattern.

How do I fix a wrong abstraction without breaking things?

Inline the abstraction into each caller, remove unused branches, accept short-term duplication, then re-extract focused helpers once patterns are clear.