This article is published in English.
Duplication Versus Coupling: Deciding When Shared Code Should Exist
Learn why merging look-alike React components or NestJS services can cost more than duplication, and use three questions to decide when an abstraction has earned its place.
Most developers are trained to treat repeated code as a defect: spot two similar components, extract a shared one, and move on. That reflex is often right, but it hides a cost that only shows up months later, when the shared piece has to serve consumers whose requirements have drifted apart. Framing the decision as a trade between two different costs, duplication and coupling, gives you a small set of questions for deciding which one to pay in React, React Native and backend code.
The core claim is simple and easy to misread: repetition is not a virtue, but in some situations duplication is cheaper than coupling. What follows is a guide to recognising those situations.
How a tidy shared component turns into a configuration language
Start with two components that render a person's picture. One is for regular users:
<UserAvatar user={user} />
The other is for trainers:
<TrainerAvatar trainer={trainer} />
On day one they are nearly indistinguishable. Each renders:
- a profile image
- a fallback when the image is missing
- identical dimensions
- a loading state
The obvious conclusion is that one component should do both jobs, so a generic avatar appears:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
/>
It looks like a clear win: less code, one place to fix bugs. Then the product evolves. Users need a placeholder that respects privacy settings. Trainers need a verification badge. Users can fall back to initials. Trainers show whether they are currently available. Each request lands on the shared component as another prop:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
showInitials={...}
showVerification={...}
showAvailability={...}
privacyMode={...}
/>
More requirements keep arriving, and every one becomes a flag. Eventually the "generic" avatar knows about users, trainers, privacy rules, verification, availability and assorted business logic that has nothing to do with drawing a circle with a picture in it. It is still technically reusable, but it is no longer simple. The complexity was never removed; it was gathered into one file, where every consumer now depends on all of it. If this prop-explosion pattern looks familiar, the article on when reusable React components backfire looks at refactoring such components in more depth.
Sharing code means sharing a future
The point that rarely gets discussed is that extracting shared code is not only a decision about implementation. It ties the consumers' futures together. When component A and component B both depend on one abstraction, any change made for A can now break or reshape B. Reuse creates a relationship, and relationships are coupling.
That changes the question worth asking. "Could these two things share code?" is almost always answerable with yes. The better question is: should these two things share a future?
Two blocks of code can be textually identical today and still belong to unrelated parts of the domain. Their implementation coincides; their reasons to change may not. That difference predicts maintenance cost far better than a count of duplicated lines. It is closely related to the single responsibility principle, usually phrased as "a module should have one reason to change", where a reason to change means a group of people whose requests drive it.
When duplicated markup buys independence
Consider a user card:
<UserCard />
and a payment card:
<PaymentCard />
Right now both render the same structure:
<div className="card">
<h3>{title}</h3>
<p>{description}</p>
</div>
A shared card is the natural next step:
<Card />
Now suppose the user-facing card is redesigned often, while the payment card is constrained by entirely separate product and compliance requirements. The matching markup is a coincidence; the business meaning is not shared. Keeping the two separate costs a handful of duplicated JSX lines. In exchange, each concept gets a place where it can evolve without negotiation. That duplication purchases something concrete: the ability to change one without coordinating with the owners of the other. Seen that way, two separate components are not sloppy architecture. They can be a deliberate boundary.
There is an important nuance for React specifically. A purely visual shell, a styled container with no domain meaning, can often be shared safely, because its reason to change is the design system rather than a product feature. The trap is when the shared piece starts absorbing domain decisions that belong to one consumer.
Ask why two things are the same, not how to merge them
Experience tends to shift the first question you ask when you see repetition. Early on, the instinct is "how do I abstract this?" A more useful sequence is:
- Why are these two things the same right now?
- Will they stay the same, and for the same reason?
The second question is harder, because it forces you to stop comparing syntax and start comparing intent.
Identical implementations can model different concepts
Take two formatting helpers, one for users:
formatUserName(user)
and one for trainers:
formatTrainerName(trainer)
Suppose both currently contain the same body:
return `${firstName} ${lastName}`;
So they get merged into a single helper:
formatFullName(person)
That feels harmless until the rules diverge. User names may need to account for:
- preferred names
- privacy settings
- localisation rules
Trainer names may need:
- professional titles
- certifications
- specific display-name policies
The two original functions could have grown along those separate paths without friction. The generic helper instead asserts that users and trainers are the same kind of "person" for naming purposes, which is no longer true. This is the less obvious risk of abstraction:
An abstraction does not just share code; it encodes a claim about how the domain is structured.
When that claim is wrong, the abstraction actively misleads the next developer, who reasonably assumes that anything called formatFullName applies to every person in the system.
The bill for a premature abstraction arrives later
Premature abstraction is attractive because every visible metric improves on the day you introduce it. The file shrinks from something like:
100 lines
down to:
60 lines
The duplication disappears, the pull request looks cleaner, and the abstraction feels elegant. The cost is deferred. A year on, someone needs to change the behaviour for one consumer and discovers that the shared component has seven others. Rather than risk breaking them, they branch:
if (variant === "A") ...
else if (variant === "B") ...
else if (variant === "C") ...
Then comes another prop, another flag, a compatibility path for an older screen. The abstraction survives, but its conceptual simplicity does not. That is how codebases end up with components whose interface looks like this:
<UniversalThing
mode="..."
variant="..."
type="..."
compact
showHeader
showFooter
enableSomething
disableSomethingElse
/>
At that point the component has stopped being an abstraction and become a small configuration language for several unrelated use cases. Every combination of those props is a state someone must reason about, and most combinations were never tested.
Reuse can make change harder, not easier
The irony is that shared code is created to make change cheaper, yet excessive sharing often makes it more expensive. The reason is blast radius: the set of things a single change can affect.
Before the abstraction, each feature owns its component:
Feature A → Component A
Feature B → Component B
After it, every feature routes through one shared piece:
Feature A ─┐
Feature B ─┼→ Shared Component
Feature C ─┘
The second diagram has less duplicated code, but a stronger dependency web. So the useful comparison is not "which version has less duplication?" but "which version makes the cost of future changes more predictable?" When a change to feature B must be regression-tested against features A and C, the shared component has made change less predictable, even though it made the code shorter.
Why React invites premature sharing
React makes extraction almost frictionless. A button appears:
<Button />
and it becomes reusable. Then a card:
<Card />
Then a modal:
Modal />
Then a form field:
FormField />
Then a custom hook:
useSomething()
Before long there is an internal component library used across the whole application. Much of it is genuinely valuable. Some things clearly should be shared:
- a button that expresses the application's design system consistently
- low-level accessibility primitives, such as focus management or accessible labelling
- domain concepts that are genuinely stable
Reuse itself is not the issue. Reusing things only because they look alike is. Shared UI primitives work well when they stay free of business rules, a point also explored in designing components around responsibility rather than reuse.
React Native multiplies the states
Mobile apps add another dimension. Two screens can look alike while having quite different lifecycle needs. A component that works well on one screen may later need to cope with:
- the app moving to the background
- keyboard behaviour
- dropped network connections
- platform differences between iOS and Android
- permissions
- deep links
- varying device dimensions
- navigation state
- offline mode
If everything is made generic up front, the shared component ends up knowing about every environment it might ever run in. It becomes "flexible", and flexibility has a price: each new option multiplies the number of possible states. Every one of those states is something a person eventually has to understand, test and maintain. Two options create four combinations; five create thirty-two.
Backend services fall into the same trap
This is not a frontend-only issue. Picture two NestJS services:
UserService
TrainerService
Initially they may expose the same operations:
create()
findById()
update()
delete()
A generic base class looks appealing:
BaseService<T>
Sometimes that works and saves real code. But once the business rules for users and trainers diverge, the base class starts collecting exceptions. First a type check:
if (entityType === "user") ...
then an overridable hook before updates:
protected beforeUpdate(...)
then another after creation:
protected afterCreate(...)
and gradually a whole set of extension points whose only purpose is to make one generic service behave differently per domain. That trades duplicated code for conditional complexity, which is usually much harder to reason about, because understanding one entity's behaviour now means reading the base class, its hooks and every override together. If this sounds familiar, the discussion of structuring NestJS domains with DDD rules offers a complementary view on where boundaries belong.
This is not an argument for copy-paste
Concluding that duplication is good would be just as simplistic. Duplication has real costs:
- If ten places implement the same business rule independently, one bug fix may need ten edits, and missing one creates inconsistent behaviour.
- If twenty components each implement the same accessibility behaviour, keeping them consistent becomes very hard.
- If several applications rely on the same stable contract, sharing that contract is enormously valuable.
The actual point is narrower: duplication and coupling are two different kinds of cost. Good engineering involves picking the cost that fits the problem, rather than always minimising one of them.
Three questions before extracting an abstraction
When two pieces of code look alike, pause and work through these before merging them.
Do they change for the same reason?
This is the most important one. If product requirements tend to change A and B together, sharing is likely sensible. If A changes because of one business driver and B because of another, today's identical implementation is weak evidence that they belong together.
Do they mean the same thing?
Code can be identical while its semantics differ, and semantics are what evolve. A price and an account balance may both be plain numbers, yet that does not justify collapsing both into a single alias everywhere:
type Amount = number;
The representation matches; the meaning does not. A price might gain currency and tax rules, while a balance gains overdraft limits. Keeping them as distinct concepts, even if both are numbers today, keeps that divergence cheap.
Is this removing duplication or just removing lines?
These are different outcomes. A good abstraction removes duplicated concepts. A poor one merely shortens files. Pulling 30 lines out of two components into a 40-line helper with eight props has not necessarily improved anything; the complexity has simply moved and gained an interface to maintain.
Deliberate duplication is a legitimate choice
It is reasonable to leave two pieces of code separate when they are:
- small
- simple
- likely to evolve independently
- not guarding a critical invariant
- not part of a shared domain concept
The reason to leave them alone is not a lack of skill with abstractions; it is an understanding of what the abstraction would cost. Being able to look at repetition and decide "not yet" is a sign of maturity, not laziness. Not every repetition is technical debt. Sometimes it is just repetition.
When duplication becomes a warning sign
The other side matters just as much. Some duplication is a clear signal to consolidate:
- the same complex business rule copied into several places
- three applications each implementing the same authentication flow
- multiple teams that must honour one API contract
- a rule change that requires remembering ten separate locations
In those cases the right question is: what knowledge is being duplicated? That matters far more than how many lines repeat, because the thing to avoid duplicating is knowledge, not text.
DRY was always about knowledge
"Don't Repeat Yourself" is commonly read as "never write the same code twice". The original formulation in The Pragmatic Programmer is about knowledge: every piece of knowledge should have a single, authoritative representation in a system. Those are different rules.
Two components can contain similar JSX without duplicating any business knowledge. Meanwhile, two functions that look nothing alike can each encode the same business rule, for example a discount threshold hard-coded in both a checkout component and a backend validator. The second case is the dangerous one, because it will drift silently. So instead of asking whether something can be made reusable, ask where this knowledge should live.
Let the abstraction earn its place
It is not necessary to design an abstraction the moment duplication appears. Letting the repetition exist for a while, and watching how the copies evolve, is a valid strategy. If the second and third use cases keep moving in the same direction, the right shape becomes obvious. The shared behaviour is then discovered rather than invented.
That difference is significant. An abstraction extracted from three genuinely similar use cases is usually far sturdier than one designed from a single use case to accommodate two hypothetical future ones. This is the reasoning behind the familiar "rule of three" heuristic. Put another way:
Abstract based on what you now understand the code to be, not on what you imagine it might become.
The goal is changeable design, not reusable code
Reuse is not a useful measure of architectural quality on its own. Better indicators are:
- how easy the code is to understand
- how isolated a typical change is
- how predictable the blast radius of a change is
- whether business concepts stay clearly separated
- whether boundaries sit in sensible places
- whether the system can evolve without fear
Sometimes those criteria lead to an elegant shared abstraction. Sometimes they lead to two nearly identical components side by side, and that can be the better design, because the two were never the same thing. They just look alike today.
Key takeaways
- Sharing code creates a dependency between consumers; treat every extraction as a decision that their futures are linked.
- Judge candidates for abstraction by their reason to change and their meaning, not by textual similarity.
- Prop flags, variant branches and overridable hooks are signs that a shared piece is serving unrelated domains.
- Duplicate small, simple, independently evolving code freely; consolidate duplicated knowledge such as business rules, contracts and security flows.
- Prefer abstractions discovered from several real use cases over ones designed for imagined ones.
- Before merging two look-alike pieces, ask whether you are creating reuse or creating a relationship, and whether that relationship should outlive the lines you are saving.