{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreighoz4kmabpen3qlqxubmbyziz3igjxwy7jpbv45vvkgznodb6xyq",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mowictzoevq2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreigfd2zur2ueaylqfacxh4u4rz3dbc6sbntoseeumfdsax6tzrqdwq"
    },
    "mimeType": "image/webp",
    "size": 528144
  },
  "path": "/norfolkd/prefer-duplication-over-the-wrong-abstraction-1kf1",
  "publishedAt": "2026-06-23T03:18:00.000Z",
  "site": "https://dev.to",
  "tags": [
    "architecture",
    "typescript",
    "frontend",
    "designsystem",
    "The Wrong Abstraction"
  ],
  "textContent": "Sandi Metz wrote The Wrong Abstraction in 2016. It keeps resurfacing — in high-scoring HN threads, architecture Slack channels, and code review comments — whenever a team is staring at a function that started as a clean DRY consolidation and has since acquired seven parameters, three feature flags, and a comment that says `# don't touch this`.\n\nThe core argument is simple: duplication is far cheaper than the wrong abstraction. Once you've built the wrong abstraction, you're not starting from a clean slate — you're inheriting someone else's sunk cost, and every new requirement gets jammed into a shape that doesn't quite fit.\n\nI've spent a lot of time at the intersection where this matters most: design systems, editor foundations, and cross-team API contracts. These are exactly the contexts where the abstraction temptation is strongest — and where getting it wrong is most expensive.\n\n##  Why the wrong abstraction is so easy to create\n\nAbstractions don't usually start wrong. They start reasonable.\n\nYou have two components that render a card. They share 80% of their structure. You extract a `Card` component, parameterize the differences, and ship it. Six months later, `Card` has a `variant` prop with eight values, a `withBorder` flag, a `compact` prop, a `headerSlot`, and a `suppressDefaultPadding` escape hatch added by someone who needed the component to do something the abstraction never anticipated.\n\nThe problem isn't that the original extraction was wrong. The problem is what happened next: every new use case was treated as a reason to extend the existing abstraction rather than a signal to question whether the abstraction still fit.\n\nMetz identifies the specific failure mode: _the abstraction is not the thing itself, it's a theory about the thing_. When the theory is wrong, new data doesn't correct it — it gets explained away by adding parameters.\n\n##  The cost of inline duplication is lower than it looks\n\nWhen two components share markup, the instinct is to extract immediately. But consider what you actually know at extraction time:\n\n  * You know what the two existing callers need.\n  * You don't know what the third caller will need.\n  * You don't know which parts of the shared structure are incidentally similar versus structurally equivalent.\n\n\n\nIncidental similarity is the trap. Two things can look identical for completely different reasons. A `StatusBadge` and a `CategoryTag` might render the same pill shape today. That doesn't mean they'll evolve together. Extracting them into a shared `Pill` component couples their evolution even if their domains have nothing in common.\n\nDuplication, in this case, is not laziness — it's preserving optionality. Each component can change shape independently when its domain requirements diverge, which they will.\n\n\n\n    // Two components that look identical today\n    const StatusBadge = ({ status }: { status: OrderStatus }) => (\n      <span className={`badge badge--${status}`}>{STATUS_LABELS[status]}</span>\n    );\n\n    const CategoryTag = ({ category }: { category: ProductCategory }) => (\n      <span className={`badge badge--${category}`}>{CATEGORY_LABELS[category]}</span>\n    );\n\n    // Six months later, StatusBadge needs a tooltip and an ARIA live region.\n    // CategoryTag needs a remove button and a count indicator.\n    // The shared abstraction would now be a liability.\n\n\nIf you'd extracted a `Pill` component on day one, you'd now be untangling it. Instead, two independent components diverge cleanly.\n\n##  When shared primitives are genuinely correct\n\nNone of this means \"never abstract.\" The argument is about _wrong_ abstractions, not abstractions generally. There's a category of shared code that genuinely belongs together: primitives that are structurally equivalent by design, not incidentally similar by coincidence.\n\nIn a design system, a `Button` component is a real abstraction. It exists because button behavior, accessibility semantics, focus management, and visual consistency are supposed to be uniform across every surface. The shared implementation isn't an accident — it's the point. When the design system updates the focus ring to meet WCAG 2.2 criteria, you want every button in the product to update from one change.\n\nThe test for a real abstraction is whether its callers are _supposed_ to be coupled. If coupling them is a feature — consistent behavior, enforced constraints, centralized correctness — the abstraction earns its place. If coupling them is just an artifact of current visual similarity, it's a liability.\n\nA useful heuristic for architecture reviews:\n\n> **Does this abstraction encode a rule, or does it encode a coincidence?**\n\nA `Button` encodes a rule: interactive elements with this semantic role should behave this way. A `Pill` extracted from `StatusBadge` and `CategoryTag` encodes a coincidence: these two things look the same right now.\n\n##  The parameter accumulation signal\n\nYou rarely see a wrong abstraction clearly at creation time. The signal appears later, in how the abstraction responds to pressure.\n\nRight abstractions absorb new requirements gracefully. You add a new button variant: `<Button variant=\"ghost\">`. The existing API handles it cleanly. The abstraction was modeling something real, and the new case fits the model.\n\nWrong abstractions resist new requirements. You need the card component to suppress its default padding in one specific context. There's no clean way to express that, so you add `suppressDefaultPadding={true}`. The prop name itself is a confession — it's not modeling a concept, it's punching an escape hatch through the existing model.\n\nParameter accumulation — especially boolean flags or parameters that only matter to one caller — is the clearest signal that an abstraction has outlived its theory.\n\n\n\n    // This function signature is a warning sign\n    function formatUserName(\n      user: User,\n      options: {\n        includeTitle?: boolean;\n        shortForm?: boolean;\n        uppercaseLastName?: boolean; // added for the export feature\n        omitMiddleName?: boolean;    // added for the badge component\n        legalFormat?: boolean;       // added for contract generation\n      }\n    ): string\n\n\nThis function is doing five jobs. It's no longer a formatting function — it's a dispatch table that routes to five different formatting strategies based on flags. The right move is to inline the relevant logic at each call site, or to create five clearly-named functions that each do one thing.\n\n##  The recovery path: inline before you re-extract\n\nMetz's prescription is specific: when you recognize a wrong abstraction, don't refactor it — _inline it_. Copy the abstraction's code back to each call site, restore the parameters to their concrete values, delete the dead branches, and see what you actually have.\n\nThis is uncomfortable. It feels like moving backward. But what you get after inlining is the truth: the actual logic each caller needs, without the mediation of a theory that no longer fits.\n\nFrom that position, you can see whether a new abstraction is warranted. Sometimes the call sites look very different after inlining — which tells you the old abstraction was hiding real divergence. Sometimes they look nearly identical — which means a better abstraction is now obvious, because you're working from real requirements rather than accumulated historical ones.\n\nI've done this with editor extension configurations, design token resolution logic, and API response normalization layers. Every time, the inline step was the uncomfortable-but-necessary precondition for getting the abstraction right.\n\n##  The cross-team dimension\n\nAt staff level, wrong abstractions have a social dimension that makes them harder to fix. When an abstraction is owned by one team and consumed by three others, inlining it requires coordination. The owning team has to be willing to let go of something they built. The consuming teams have to accept temporary duplication. Everyone involved has to resist the pull toward \"let's just add another parameter.\"\n\nThis is where architecture reviews and RFCs earn their keep. The time to debate whether an abstraction is modeling the right thing is _before_ three teams build on top of it — not after. A good RFC process forces the question: is this abstraction encoding a rule that should govern all callers, or is it encoding one team's current needs in a shape that will constrain everyone else later?\n\nThe answer isn't always to avoid the shared abstraction. Sometimes the shared primitive is genuinely the right call. But asking the question explicitly, with concrete examples of what future callers might need, catches a lot of wrong abstractions before they get adopted.\n\n##  The actual takeaway\n\nMetz's essay persists because it names something that's easy to see in retrospect and hard to see in the moment. The wrong abstraction feels like good engineering when you create it. It reduces line count, eliminates repetition, and looks clean. The cost shows up later, incrementally, as each new requirement gets shoe-horned in.\n\nThe discipline it requires is specific: resist the extraction until you understand what the shared structure actually represents. Duplicate freely when the similarity is incidental. Extract confidently when the abstraction encodes a real rule. And when you inherit something that's accumulated enough parameters to be unreadable, have the resolve to inline it before you try to fix it.\n\nDuplication is recoverable. The wrong abstraction is a debt that compounds.",
  "title": "Prefer duplication over the wrong abstraction"
}