back
354 comments
I believe that "single source of truth" is a principle that should always be followed. If there's duplicated code where it'd be a bug if they diverge, then you should refactor. It creates a long-distance coupling in your code that may be invisible to future developers until a bug emerges.

But with that in mind, I mostly agree with the article: if it's not a violation of "single source of truth", then abstractions are just a convenience. If it starts being inconvenient, then it's not doing its job and there's no reason to use it. It's a serious code smell if a function needs several flags for custom behavior; that means it's probably the wrong abstraction or violating the single responsibility principle. If there is a legit need for lots of customization, an often-good way to handle is to take a function/functor as an argument for the customization. E.g., rather than `solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)` you can do `solve(f:double -> double, stopping_criteria: StoppingCriteriaClass)`

> I believe that "single source of truth" is a principle that should always be followed

Fundamentally, the article addresses cases where it's not clear yet how many sources of truth there will be. Are the two spots in the code using the same algorithm, or slightly different versions? More importantly, will they change for the same sorts of reasons?

The title adage (correctly, imo) argues that making two different things the same will cause you more pain than making two same things different via duplication. In the latter thing case, the "damage" is just having to make the same changes twice, or doing a refactor to introduce the abstraction. In the former case, you have to keep adding to your abstraction, or undo it. Most crucially, it breaks "locality", which is the only property you really care about when making changes. I just want to make this change and not worry about side effects to unrelated parts of the system.

One killer life hack I’ve found is, if extreme duress pushes software into two sources of truth, add a ci test that wont merge into main till the sources match. The canonical case of this actually being the best solution is pyproject.toml / requirements.txt synchronization, but I suspect it has broader applicability. A precondition is that things have already gone off the rails far enough that single source of truth is unattainable, this is more harm reduction than cure
> it'd be a bug if they diverge

That's a very nice rule of thumb. I've often overabstracted when two pieces of code look similar at one point in time and then they diverge.

> I believe that "single source of truth" is a principle that should always be followed

Theoretically and conceptually I agree. But in practice there are a lot of programming languages aren’t as expressive. People prefer codebases with duplications rather than visitor patterns everywhere. In essence, visitor pattern is a tool to solve multi-dimensional abstraction problems, just like type classes in Haskell or CLOS in Common Lisp. But it’s so verbose and non-straightforward so more often than not it’s not worth it even conceptually it’s a legit case for “single source of truth”.

Of course, in theory this is true. In practice people tend to avoid ANY duplication no matter what. Especially junior developers, as if duplication would be the root of all evil.
If you knew in advance which source of truth is important to isolate you don’t have this problem.

The problem is not knowing which of the hundreds or thousands of potential truth sources is worth abstracting. The only real way of finding out is not abstracting them and seeing how it works out.

If the problems in SWE boiled down to solve(f -> MagicallyNoProblemAnymore) we wouldn’t have this discussion.

With LLMs the cost of duplication is much lower and LLMs
Code duplication differs from single source of truth applied to data in the sense that data is data but two pieces of code may functionally be the same (they do the same thing) but they might be semantically different in their usage (they’re advertised to achieve different things), in that case coupling them together with deduplication and forcing them to do the same thing doesn’t really make sense, and may make the codebase more difficult to work on in the future (especially in companies where different teams have responsibilities over different parts).
> If they diverge

This is the key, if they are very similar but used by different consumers the chance that they will diverge in the future is very high. And once they do they will break the abstraction.

I think about this on occasion. Most recently I ran into an issue during a personal project: 2d sprites for RTS units were packed on spritesheets in a consistent manner: 5 sprites for 8 directions (you mirror 3). Packed in order of: stand, move, attack, die. So I made a loader that understands how to take action + direction and offer an array of sprites to play through.

But then I came across more cases: sprites with no directionality (an explosion), and corpse sprites (which were only 4 directions, 2 mirrors, and most except the first four were shared by both orcs and humans).

I agonized for a little bit on what the hell the common abstraction is for all this. In the end, I factored out some of the loading code, and made a UnitLoader, CorpseLoader, EffectLoader and moved on. Now, there's probably a better abstraction in there because all 3 loaders have to reason about the same things a little bit. But I will discover that abstraction later on and it's easier to just de-duplicate the code then, rather than try to identify the abstraction now and make some complicated EverythingLoader that handles all those cases.

I used to struggle with abstractions back in my OOP days but since moving pretty much to a purely functional approach I find that code duplication is rare. Just have a function and call it in two parts. The main abstraction issue is then data structures but with TypeScript interfaces being duck typing essentially I run into few problems there as well.

So code duplication because of abstraction issues is rare. Code duplication because of siloed developers is so much more common.

Two talks come to mind here: Mike Acton's Data-Oriented Design and C++ [1] and Brian Cantrill's The Complexity of Simplicity [2].

Mike's talk argues that code solutions need not be modelled on the real world, and that different data creates different problems, which need different solutions. I can't do the talk justice, but it's had a big impact on me.

Brian's talk is about abstraction generally, and how it's difficult to find the "right" abstraction.

1. https://www.youtube.com/watch?v=rX0ItVEVjHc

2. https://www.youtube.com/watch?v=Cum5uN2634o

Echoing the article, anyone who has experienced both will agree: it’s far easier to work with an under engineered code base than an over engineered one.
+1 The worst code I had to maintain was code that tried to follow DRY (without the trying to understand what the original intention of that principle was). The only way out of that mess was widespread code duplication.
Nobody wants to listen. Nobody. In 90% of the companies there are some so called senior devs that get ecstatic when they create a new abstraction.

Overengineering, abstractions and premature optimisation are the 3 worst plagues of engineering.

At the same time I’m happy they exist because it means we’ll always have a job.

Similarly, I've seen some developers who seem to think that any inline string or numeric constant is evil. In one PR, I saw:

    HTTPS_SCHEME = 'https'
    DOMAIN = 'www.example.com'
    
    url = HTTPS_SCHEME + '://' + DOMAIN
I don't understand what they think this is buying, other than just cargo culting "don't embed constants." And of course, the constant definitions were at the top of the file and the url building code was hundreds of lines away.
You can do both with microservices!
Too many abstractions are bad. Too many code duplication is bad.

Part of being a good engineer is finding the right balance.

I know engineers who would gladly duplicate code all over the code base to avoid creating a new abstraction.

I know engineers who create polymorphic abstractions for a single caller with a very obvious set of parameters.

So much of wisdom is in finding balance and not being dogmatic about rules.

i recall very early in my career i did exactly this. i took what worked duplicated it—my reasoning being that it was far safer to reuse what has been battle tested and leave refactoring at a later stage

it wasn't received well and senior developer told me that 'good developers know exactly what patterns to use all the time before writing any piece of code and that he will clean up my mess'

long story short his refactoring caused what was otherwise a stable system into a complete mess and it reminded me of Nassim Taleb's book

I dislike duplicate code as much as anyone, but agree with the OP that bad abstractions can be worse. They add confusion and complexity which compounds over time, since people are forced to build on top of them in ways that (by definition) don't suit the underlying domain and ultimately become self-referential. This leads to contortions, workarounds and even more bad abstractions which ought not to be there—they're reactions to the code not fitting the problem, or as Fred Brooks called it, accidental complexity. You end up in an evolutionary dead end where the system is hard to extend because it's too hard to understand.

I've learned to tolerate a small amount of duplicate code for this reason. If the duplication remains small, it's not that harmful, and if it starts to grow, one has a better shot at finding a good abstraction for it. Bad abstraction is premature abstraction.

One thing I'm not sure this thread has mentioned yet is how LLMs alter the cost-benefit curve of this. They are much better at managing duplication than humans are, and much better at noticing inconsistencies - the sort of small bugs which duplication traditionally leads to. I don't know if this is enough to count as a different kind of good abstraction; I doubt it. It reminds me of a petroleum economist I once knew who had 200 duplicate spreadsheets analyzing different projects and who hired a junior analyst to keep them all consistent. An LLM would be like the junior analyst.

No it's not. This has always been a needlessly iconoclastic rather than sensible suggestion.

At the very least it is not once you're working at the wrong kind of scale.

Once you have an awkward number of customers (more than five and less than a hundred), maintaining duplicated code that should have been abstracted and modularised will only seem cheap if you don't mind that you burn through even junior employees at a pace.

And in the LLM era the wrong kind of scale appears in different ways; code generated and duplicated without proper abstraction and then maintained by an LLM that cannot be trusted to do the same modification each time it encounters a pattern or to have enough of an overview to slowly rescue duplicated code through good abstractions.

I would go as far as to say that any abstraction you can maintain (that is in active maintenance, I mean) is better than code duplication once you are past a de minimis threshold.

I once used code duplication to implement a fourth type of dialog that looked somewhat similar to the others, that were sharing a lot of code, because I felt that although it looked much the same as the others, there was some fundamental difference. Took me about a day to implement. When some other engineer saw this, he spend the next three weeks trying to integrate all of them with some shared class. His work was not completely worthless, because he did find some small bug during all his efforts to avoid any possible code duplication. I already had predicted that it would take a lot effort, but I did not object, because I hoped that he would learn something from it and the next time think twice before always trying to avoid code duplication.
2016 (up to 2018 or so) may have been the peak of such varied activity in the developer ecosystem, including articles like this, whether it was discussion, ideation, OSS variety, language development.

There has been growth since but it's been concentrated into fewer channels and somewhat industrialized.

Nice to see Sandi mentioned! If anyone liked her philosophy / writing style I highly recommend you check out her books

I read “Practical Object-Oriented Design (POODR)” ages ago at this point, but it reshaped how I approached OOP

Granted… OOP as a default paradigm has fallen out of favor (at least for me), but it’s still everywhere & won’t be going away. She gives a great framework for making it sane

I like to think most seniors know to not blindly follow DRY. However, I can tell many of us are uncomfortable with the idea of needing to maintain multiple duplicated sources of code.

To help with that, I think the simple model of two callers depending on a common code needs to be scrutinized. If the common code needs to change because only one of the caller needs it, then it doesn’t belong in the common.

The wrong goal for DRY is attempting to do it with encapsulation. Encapsulation shifts the refactoring work from the caller to the common code. However this is not what you want because there’s a lot more consequence in updating the common code than the caller.

You can avoid encapsulation and still be DRY by having multiple thin abstractions that the caller needs to be aware about is better. In OOP you are taught SRP and IoC for this. In procedural programming, this just comes naturally as code calling series of helper functions.

Related. Others?

The Wrong Abstraction (2016) - https://news.ycombinator.com/item?id=35927149 - May 2023 (69 comments)

The Wrong Abstraction (2016) - https://news.ycombinator.com/item?id=27095503 - May 2021 (17 comments)

The Wrong Abstraction (2016) - https://news.ycombinator.com/item?id=23739596 - July 2020 (240 comments)

The Wrong Abstraction (2016) - https://news.ycombinator.com/item?id=17578714 - July 2018 (207 comments)

Prefer duplication over the wrong abstraction - https://news.ycombinator.com/item?id=12061453 - July 2016 (96 comments)

The Wrong Abstraction - https://news.ycombinator.com/item?id=11032296 - Feb 2016 (119 comments)

Yes. I’m dealing with a graphql, urql, Next, Prisma stack at the moment. Something that would be a handful of lines of code in a different stack ends up being hundreds in this one.

The Node ecosystem is full of wrong abstractions.

It depends if duplication is accidental or real. I.e. if two taxes are using the same formula, it is accidental. If you use the same physic formula on multipla places, it is real duplication.
This is like saying "A slow leak is cheaper than a burst pipe"

Yes, okay. But with both you will have a bad time cleaning up.

There is a third option: good abstractions.

I did see this pattern described in the blog in practice a lot (and fell victim to it myself) and I think that in general this comes down to inexperienced programmers. Object oriented programming makes it worse.

Teaching these programmers that they should not abstract is not the solution. It is blocking their growth.

Teach them how to make better interfaces instead.

"If you have a procedure with ten parameters, you probably missed some." -- Alan Perlis
To me it’s distracting to think about duplicating vs creating an abstraction, because the answer is always “it depends”, which is not really an answer.

To me, the question is: can you look at this abstraction and understand why it exists, without knowing who’s calling it? If so, it’s probably fine.

If an abstraction only makes sense because of the particular weird details of these 3 callers that have to pass mutually exclusive arguments to it to get their desired behavior, it’s probably wrong. An abstraction needs “a place to live” in your architecture. It needs to be self-evident in justifying its existence.

If you find yourself repeating code, but de-duping it would create these sort of weird non-self-justifying abstractions, your architecture is probably a bad fit for the problem you’re trying to solve. Maybe that’s because the problem changed since the software started (which is a bit of a pickle: do you re-architect, or do you continue writing weird inscrutable code?) or maybe it’s because you just picked the wrong abstraction in the first place. But you should recognize it: duplicating vs wrong-abstraction is about choosing the lesser evil. If the abstraction was a natural fit for the problem, you wouldn’t need to answer this question in the first place.

That's why you never hire juniors as tech leaders nor "architects"
Cheaper is skipping a step.

Code duplication and 'wrong' abstractions both count themselves amongst the other foibles of programming. But they don't directly produce a cost which can be cheap or expensive.

They produce some other high dimensional intermediate value which can then produce highly variable cost dependent on the domain, goals, and scenario.

As ever, it depends.

The depends is quantifiable, but it doesn't fit in a blog post. Think more along the lines of war and peace.

Whilst I understand cases in which duplication is preffered. I generally think abstractions are underused. Sometimes I would abstract something away that is only done once, not because i want to have less code, but because it allows me to solve bigger problems and when i look at a function I don’t have to worry about it. It allows to create systems. Obviously your abstractions should be good
You dont know immediately if something that superficially seems the same actually is.

Copy and paste once is fine, twice, not so much.

Often I've seen two totally different things exist in one bit of code, no overlap!

Premature generification is bad, and leads the developer to believe that two things are the same, making it harder to see they are not.

Also, can make it much harder to see that a different abstraction would give a cleaner outcome....

Twice a coincidence, thrice a pattern.
I once had to work with a system that was refactored and abstracted away heavily to use Redux. It didn't work then, the implementation had way too many abstractions, doing any change meant you had to touch dozens of files. It was insanity. Left me with a bitter taste regarding the redux pattern for ever (probably not the pattern's fault).
While I see the point, I think I more often encounter the opposite. Duplication, but not exactly duplication. Then the "sunk cost fallacy" is not an issue but there is huge maintenance cost and no-one feels like refactoring it. I'd rather refactor bad abstraction than 10x duplication.
Duplication is fine, triplication and above is the issue.
I always liked the advice "Abstract for replacement. Not for reuse."

If you have code that is reusable, you'll want it to have a nice interface. But, you don't need an abstraction on top of a nice interface. Just use it.

For abstraction, what you need to focus on is "What is most likely to change in the future?" You want to put in abstractions that will make those changes low-cost.

Ex: At work there was a small debate about which C++ JSON parser to use with no stand-out winner for our framework's needs. So, we picked one and I put a thin layer over it for everyone to use. We have since then swapped out the parser and swapped it back over the years of a hundred devs using it in our framework and no one noticed the swaps.

Depends. If the abstraction is just a level of indirection, then it is usually pretty simple to eliminate - just hit “inline function” in the refactoring tool a few times.

On the other hand it is pretty difficult and error prone to consolidate duplicated code which have drifted apart over time.

If in doubt, chose the approach which is simplest and least risk to revert if you discover in the future you made the wrong choice.

I do agree a bad abstraction can cause huge problems. But it’s usually not the kind of abstractions introduced to eliminate code duplication, but the kind of top-down “architecture astronaut” abstractions, where a model is chosen which does not fit the complexity of the problem.

I watched a talk by her about this, and this post is missing half of the equation, which is really important:

Having a wrong abstraction means you end up with a class/function/module with a huge amount of configurations through boolean/enum parameters. It's not even clear that all combinations of configurations is even valid. This situation may be simplified by duplicating, and then eliminating code, thus creating more streamlined code for each use case. This may require fixing similar or cross-cutting bugs in multiple places (eg: JSON serialization is stupid, need to hack a workaround), but keeps the business logic changes simple. Maybe a bit more numerous, but the code is able to raise all the scenarios to consider.

Having no abstraction means you may have to change business logic consistently in multiple places, or you have to fix exactly the same misconception (aka a bug) in multiple cases. e.g. tax rate management in a multi-national context. This is also terrible, because you may fix an important problem in one place and forget other places with the same issue. Now you missed 12 potential bugs by fixing one. This can however allow you to discover a true abstraction. Maybe these 12 places should call just one place?

But for code evolving across a team understanding this tension, a bit of duplication while waiting for confirmation that these pieces of code break together and change together is better than just shoving the same 3 if-statements into a function to avoid "line duplication". Concept duplication is more important.

> Programmer A sees duplication.

This step should also be parameterized by how many times the duplication has occurred. Refactoring preemptively may lead to poor abstractions, but not refactoring after seeing the exact same thing tens of times would also be weird. See also:

https://wiki.c2.com/?DuplicationRefactoringThreshold

https://wiki.c2.com/?ThreeStrikesAndYouRefactor

I always try to design in a way, that using abstractions/shared logic is optional.

I've worked in too many projects, where every new feature needs to be built on top of existing abstractions, that often lead to severe restrictions if something slightly different is required. I always try to create reusable units/components, that can either be used as intended or replaced by something that behaves slightly different if needed.

Components are not necessarily frontend components, this extends also to backend logic.

One way I like to think about is that often abstraction is an automation for a task that doesn't need automating.

You hardly ever change the thing and if you do, changing it in two or three places 'manually' is really not a big deal.

Now changing something fairly often, that affects logic in 50+ places? Then it makes sense to automate with an abstraction so it all flows through the same lines of code.

I know I've personally spent way more time over the years debugging bad abstractions than changing things in a few places.

I feel this deeply. Although abstraction isn’t a one way door, “deduplicating” logic tends to be much easier than breaking big functions back down, and so these days I tend to leave a comment with the date wondering if it is too similar to some other code. then if i come across it again months later and it still is, then maybe it is safe to make it DRY.

I think DRY is the first heuristic for “good code” that most junior devs actually grasp, and so they become very dogmatic about it for a while

How I see this:

Refactoring code to reduce the number of lines is _compression_, akin to RLE coding.

Refactoring the code to lift conceptually coherent parts is _abstraction_.

Less compression, more abstraction. Then you're fine.