Skip to content
Sultan Kautsar

Technical note

When Not to Abstract Your Code

How to choose local clarity over premature abstraction and extract shared code only after its pattern is understood.

Author
By Sultan Kautsar
Published
Updated

import Link from "next/link";

Two pieces of code can look almost identical and still represent different ideas. Giving them one implementation may remove a few repeated lines, but it also creates a relationship: both callers must now change through the same interface. That relationship is often more consequential than the duplication it replaces.

Abstraction is useful when it expresses a stable concept. It is less useful when it merely compresses code or predicts reuse that has not appeared. The important question is not whether code can be shared. Most code can. The question is whether the callers share behavior, ownership, and reasons to change.

{"Duplication is visible; coupling is structural"}

Duplication has an obvious cost. A correction may need to be made in two places, and one copy can drift from another. Coupling has a quieter cost. Once two paths depend on the same abstraction, a change required by one path can complicate the other. Parameters accumulate, conditionals split the shared behavior, and a small local change becomes a negotiation with distant callers.

This does not make duplication harmless. It means that duplication and coupling are competing costs, not a simple bad-and-good pair. Repeating a small calculation in two bounded modules can be cheaper than introducing a shared utility that makes those modules evolve together. Conversely, duplicating a security rule, financial calculation, or protocol contract is dangerous because every copy is expected to preserve exactly the same meaning. The semantics, not the line count, determine the better choice.

Do not hide behavior that is already simple

A named function can clarify intent, but an extra layer can also make a direct operation harder to inspect. If understanding a three-line transformation now requires opening a generically named helper, tracing options, and checking defaults, the abstraction has moved information away from the place where it matters. It has not reduced complexity; it has redistributed it.

Local code is especially valuable when the details explain the policy. A database query can show which records are eligible. A request handler can show the order of validation and side effects. A deployment script can show which failure stops the release. Wrapping these details behind a broad helper such as processData or runTask removes evidence that a reader needs to evaluate correctness.

Good abstraction should make the relevant idea more visible while hiding incidental mechanics. If the mechanics are the idea, keeping them local is often clearer.

Reusable components can predict the wrong future

Frontend code makes speculative reuse easy to recognize. A component starts with a few props, then gains variants for alignment, spacing, optional controls, data shape, and responsive behavior before a second real use exists. The result is not yet reusable; it is configurable. Its API encodes guesses about consumers that may never arrive.

The same failure appears outside React. A backend service may introduce a generic repository before its queries are known. A command-line tool may build a plugin system for one command. An infrastructure module may expose every provider setting in anticipation of environments that do not exist. These abstractions demand names and contracts while the domain is still uncertain. Later requirements then have to work around those early guesses.

Build the concrete path first when the variation is hypothetical. A local implementation is easier to replace than a premature public interface, and real usage supplies better design constraints than imagined examples.

Let domain boundaries outrank resemblance

Similar code that belongs to different domains may deserve to remain separate. Customer onboarding and employee provisioning might both create an account, send a message, and record an audit event. Their rules, owners, risks, and change schedules are still different. Sharing a single workflow because the current steps align can blur those boundaries and make future divergence expensive.

The reverse is also true: code that looks different may implement one domain rule and should converge behind a clear contract. Multiple entry points that calculate the same entitlement should not invent independent interpretations merely because one receives an HTTP request and another consumes a queue message. A sound abstraction follows shared meaning across delivery mechanisms.

Ownership, contracts, and reasons to change are central to how I frame engineering systems. A boundary should protect a coherent decision, not just make a directory look tidy.

Wait until repetition teaches you the pattern

The first implementation reveals the problem. The second reveals what may be common. A later implementation often reveals which similarities are essential and which were accidental. Waiting is not passive: it is a way to gather evidence before committing callers to an interface.

A pattern is usually ready to extract when:

  • the repeated code expresses the same domain concept;
  • its callers need the same guarantees and failure behavior;
  • differences can be named without a growing set of mode flags; and
  • the new interface is easier to explain than each local version.

At that point, extraction is more than deduplication. It gives a known concept one place to enforce its contract. Tests can focus on behavior that genuinely must remain consistent, while callers retain control over details that belong to their own context.

Choose deliberate simplicity

Refusing every abstraction would leave important rules scattered and force readers to reconstruct the same concept repeatedly. Abstracting at the first resemblance creates the opposite problem: generic interfaces, hidden behavior, and coupling without a stable reason.

The practical middle is deliberate simplicity. Keep code local while its meaning is still changing. Accept small, visible duplication when it preserves independent evolution. Extract only when repetition has made the shared concept, boundary, and contract clear. The goal is not the fewest lines or the fewest abstractions. It is code whose structure tells the truth about the system.