Skip to content
José da Cruz

Enterprise Architect, and Life Thinker

José da Cruz

Enterprise Architect, and Life Thinker

The Rule of Three: A Practical Heuristic for When to Actually Build That Abstraction

josedacruz, August 3, 2026August 3, 2026

Every developer has felt this itch: you copy a chunk of code for the second time, and something in your brain screams “this is wrong, I should make this reusable.” So you stop, build a shared function or class, and wire everything to use it. Sometimes that’s a great move. Sometimes it’s the start of six months of pain.

There’s a simple heuristic that experienced engineers use to tell the difference, and it’s called the Rule of Three. It’s not a law of physics. It’s a rule of thumb: a rough guideline that works most of the time, even if it isn’t perfect every time. But it’s one of the most useful tricks you can carry into your next project, because it protects you from the two opposite mistakes that eat up the most engineering time: duplicating code forever, and abstracting it too soon.

The Duplication Trap (and Its Evil Twin)

Most junior developers are taught one rule early: “Don’t Repeat Yourself,” often shortened to DRY. Repeating code is treated as almost a sin. See the same 10 lines twice? Extract a function. See it a third time? You’re probably already annoyed with yourself for not doing it sooner.

That instinct is good, but it has a dark side. If you abstract too early, based on only one or two examples, you’re guessing at a pattern that doesn’t fully exist yet. You end up with a shared function that has three boolean flags, a config object nobody understands, and branches for cases that don’t actually share any real logic; they just looked similar the day you wrote them. This is often called the “wrong abstraction,” and fixing it later costs more than the duplication ever would have. A well-known saying among experienced engineers is that duplicate code is far cheaper to deal with than the wrong abstraction, because duplicate code is at least easy to understand and easy to delete.

Meet the Rule of Three

The Rule of Three says: don’t extract a shared abstraction the first time you see duplication. Don’t even extract it the second time. Wait until you see the same logic show up a third time, in a third independent place, before you generalize it into shared code.

Why three, and not two? Because with only two examples, you can’t yet tell whether you’re looking at a real, recurring pattern or just a coincidence: two pieces of code that happen to look alike today but will drift apart tomorrow. A third occurrence, especially from a different context or a different team, gives you real evidence. It tells you what actually varies between the cases and what’s genuinely constant. That’s the information you need to design a good abstraction instead of guessing at one.

A Real Example: The Shipping Cost Calculator

Imagine a mid-sized e-commerce company with three product teams: Checkout, Marketplace, and Subscriptions. Each team owns a separate part of the customer experience, and each one, at some point, needs to calculate shipping cost for an order.

In week 1, the Checkout team writes a function that calculates shipping cost based on order weight and destination zip code. It’s specific to their flow, buried inside their code, and nobody else even knows it exists.

In week 6, the Marketplace team needs something similar for third-party sellers. They don’t know the Checkout function exists (or if they do, it’s tangled up in Checkout’s code and hard to reuse), so they write their own version. Theirs also factors in seller-specific handling fees, so it’s not identical, just similar.

In week 14, the Subscriptions team launches recurring boxes and needs shipping cost logic too. Now you have three separate implementations, in three different parts of the codebase, all doing roughly the same core calculation, but each with its own small twist.

This is the moment the Rule of Three kicks in. Before week 14, you only had two data points; not enough to know which parts of the logic were “the real rule” and which parts were one-off quirks. Now, with three real examples in front of you, a senior engineer can sit down and actually see what’s constant across all three (the core weight-and-distance calculation) and what varies (handling fees, seller rules, subscription discounts). That’s exactly the information needed to design a shared pricing module that fits all three teams instead of forcing an awkward one-size-fits-all function on people who didn’t ask for it.

Timeline showing shipping cost logic duplicated across three teams over 16 weeks before being extracted into a shared module
The pattern only became visible once it repeated a third time, in week 14. Extracting earlier would have meant guessing.

Why Not Just Abstract on the Second Copy?

It’s tempting to jump straight from “I see this twice” to “let me build the reusable version.” The problem is that with two examples, you’re really just looking at one comparison. You don’t know yet which differences between them are meaningful and which are accidental. If the Checkout and Marketplace teams had tried to merge their shipping logic in week 6, they’d have had to guess at how future teams might need to extend it. Guesses like that tend to be wrong, and once other code depends on a wrong abstraction, changing it later means touching every caller, not just one function.

Waiting for a third, independent example doesn’t mean being lazy about code quality. It means collecting enough real evidence before committing to a design, the same way you wouldn’t redesign a product after just one piece of customer feedback.

How to Tell Real Duplication from Coincidence

Not every repeated pattern deserves to become shared code, even after three sightings. Sometimes code just looks alike on the surface, while the underlying reason for that shape is different in each place, and those reasons tend to diverge over time. A useful gut check: if you changed a business rule in one of the three implementations, would you expect the other two to need the exact same change? If yes, that’s real duplication worth merging. If the answer is “it depends” or “not necessarily,” you might be looking at coincidental similarity, and forcing them together will just make future changes harder, not easier.

Decision tree for deciding whether to extract a shared abstraction after seeing duplicated code
A simple decision path: wait for a third occurrence, then check whether the duplication is real or just coincidental before extracting anything.

Applying the Rule Practically

Here’s a short checklist you can actually use the next time you’re staring at similar-looking code:

  • Seeing it for the first time? Just write the code. Don’t design for reuse you haven’t proven yet.
  • Seeing it for the second time? Note it, maybe leave a comment, but resist the urge to abstract immediately.
  • Seeing it for the third time, in a genuinely separate context? Now you have enough evidence to design a real abstraction.
  • Before extracting, ask: would a change to the business rule in one case really need to happen in the others too? If not, keep them separate.
  • When you do extract, design the shared code around what’s actually constant across all three real examples, not around what you imagine future callers might need.

This rule isn’t limited to functions and classes. It applies to database tables, API endpoints, internal services, even Terraform modules. Any time you’re tempted to generalize something, the same question applies: do I have enough real, independent examples yet to know what the general shape actually is?

The Takeaway

The Rule of Three won’t make every abstraction decision easy, but it gives you a concrete trigger instead of a vague feeling. Two examples is a hunch. Three independent examples is a pattern. Waiting for that third occurrence costs you a little short-term duplication, but it saves you from building the wrong shared code and having to unwind it later, which is almost always more expensive. Next time you feel that itch to abstract after seeing something twice, it’s worth pausing and asking: have I actually seen this enough times to know what it really is?

Related

architecture abstractioncode duplicationengineering practicessoftware design heuristics

Post navigation

Previous post
Next post

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • architecture (154)
  • artificial-intelligence (6)
  • books (2)
  • courses (4)
  • decision-frameworks (4)
  • finances (1)
  • java (8)
  • life-improvement (20)
  • metrics (3)
  • observability (2)
  • puzzles and challenges (3)
  • reviews (1)
  • security (8)
  • spring-boot (36)
  • Systems Thinking (3)
  • GitHub Weekly: MCP Tools Trending Right Now (September 28 – October 4, 2026)
  • The Bulkhead Pattern: Why One Slow Dependency Shouldn’t Sink the Whole Ship
  • Essential Reading: A Philosophy of Software Design — Fighting Complexity One Module at a Time
  • GitHub Weekly: Top 10 Trending Repos (September 21 – September 27, 2026)
  • The Volunteer Tax: Why Raising Your Hand in a Meeting Makes You the Permanent Owner
©2026 José da Cruz | WordPress Theme by SuperbThemes