Skip to content

Rule of Three

When to Use

When deciding whether it's time to refactor duplicated code into an abstraction.

The Principle

"Three strikes and you refactor" — attributed to Don Roberts, popularized by Martin Fowler in "Refactoring."

Instance Action Reasoning
First Write the code No pattern yet, no evidence of reuse
Second Note the duplication Pattern might be emerging, but still unclear
Third Refactor to abstraction Now you understand the variance and commonalities

Decision Framework

If... Then... Why
This is the first time you've needed this logic Write it inline No evidence it will be reused
This is the second time Duplicate it (WET approach) Wait to see if a third instance clarifies the pattern
This is the third time Abstract it (DRY approach) You now have enough context to abstract correctly
Requirements are still changing rapidly Wait even longer Abstraction may ossify before requirements stabilize
All instances always change together Abstract sooner Strong signal they represent same knowledge
Instances sometimes diverge Keep separate longer May be incidental duplication

Why Wait for Three?

After one instance: You have no idea if the pattern will recur.

After two instances: You don't yet understand the variance. Are these truly the same knowledge, or coincidentally similar? What parts are stable, what parts vary?

After three instances: You can see the pattern clearly. You understand what's common, what's variable, and what the abstraction should look like.

Pattern

# ITERATION 1: First instance — write it
def export_users_csv(users):
    output = StringIO()
    writer = csv.writer(output)
    writer.writerow(['ID', 'Email', 'Name'])
    for user in users:
        writer.writerow([user.id, user.email, user.name])
    return output.getvalue()

# ITERATION 2: Second instance — duplicate it (WET)
def export_products_csv(products):
    output = StringIO()
    writer = csv.writer(output)
    writer.writerow(['ID', 'SKU', 'Name', 'Price'])
    for product in products:
        writer.writerow([product.id, product.sku, product.name, product.price])
    return output.getvalue()

# NOTE: Don't abstract yet! Wait to see if third instance clarifies pattern

# ITERATION 3: Third instance — NOW abstract
def export_orders_csv(orders):
    # Pattern is now clear: headers + row transformation
    pass  # Don't implement — refactor instead!

# REFACTORED: Abstract with confidence
def export_to_csv(items, headers, row_mapper):
    """
    Generic CSV exporter.

    Args:
        items: Iterable of objects to export
        headers: List of column headers
        row_mapper: Function that transforms item to list of values
    """
    output = StringIO()
    writer = csv.writer(output)
    writer.writerow(headers)
    for item in items:
        writer.writerow(row_mapper(item))
    return output.getvalue()

# Usage — clean and DRY
users_csv = export_to_csv(
    users,
    ['ID', 'Email', 'Name'],
    lambda u: [u.id, u.email, u.name]
)

products_csv = export_to_csv(
    products,
    ['ID', 'SKU', 'Name', 'Price'],
    lambda p: [p.id, p.sku, p.name, p.price]
)

orders_csv = export_to_csv(
    orders,
    ['ID', 'Customer', 'Total'],
    lambda o: [o.id, o.customer_id, o.total]
)

Exceptions to the Rule

Abstract earlier when:

  • All instances are guaranteed to change together (true knowledge duplication)
  • It's a well-known, stable pattern (e.g., retry logic, logging)
  • Code is in hot path and performance demands consolidation
  • Legal/compliance requires single source (e.g., tax calculation)

Wait longer when:

  • Requirements are highly volatile
  • Instances serve different business domains
  • Cost of wrong abstraction is high (public API, shared library)
  • Team is still learning the domain

Common Mistakes

  • Abstracting at instance #1 or #2 — High chance of wrong abstraction, will need to undo later
  • Rigidly applying "three" as law — Context matters; use judgment
  • Never refactoring even after many instances — Technical debt accumulates, maintenance nightmare
  • Treating "three" as maximum — If 5-6 instances exist with no abstraction, you're late to refactor
  • Ignoring the "pain signal" — If duplication causes real pain (bugs, hard to change), abstract regardless of count

See Also