⚠️ A quick word of warning, I am not a carpenter and have no real training in wood working and none of what is written here should be used for structural engineering or anything that requires accurate information on lumber.

The DRY methodology (otherwise known as Don’t Repeat Yourself) has been touted for quite a while as the way to make sure your code is easier to maintain and is cleaner. The basic premise being that if you find yourself repeating the same type of logic then it should probably be abstracted away so that common code can be reused in multiple places and any changes can be made in one place instead of multiple.

That sounds pretty good right? If you find a bug in some common logic you would like to have confidence that it’s been fixed everywhere and that you only have to make that fix one time versus copy/pasting the fix everywhere. Plus everyone knows that less code is better right?

There are downsides though to DRYing up your code though and this is where we’re going to start comparing code to lumber.

Say you cut a branch from a tree and tried to build something with it. Ignoring the fact that it’s not going to be a super great shape (usually we prefer building with rectangular materials) you’re going to find that it’s very bendy and that it’s not going to be great for bearing a lot of load. Fresh wood also has more water in it meaning its going to be heavier which may be problematic depending on what you’re trying to construct. That’s why, along with reshaping, we dry and treat wood before using it. By drying it out we’re making the wood stronger, less bendable, and a generally better building material. If I was going to be building a house from scratch I would probably want to pick prepared lumber instead of really fresh wood.

There’s a downside to this drying process though. By removing all the water from the wood we’re making it more brittle and less able to handle changes in stress. Imagine bending a really dried out stick and a really supple, fresh stick. The dry stick is going to break well before the fresh stick. You may not even be able to fully break the really bendy stick! You might have to really twist, contort or even cut it before you get a complete break.

This implies that both forms of wood have their strengths and their weaknesses and could be used in different situations. The same is true of DRY vs WET (write everything twice (or more)) code.

When you DRY out code you’re making it easier for other people to build upon by providing a (hopefully) solid foundation for their work. But at the same time the more you DRY out your code and reuse it in more places the harder it is to change that code without breaking things or having unintended side-effects.

Unfortunately I’ve seen a tendency among developers towards immediately DRYing out their code as soon as they see any amount of duplication. This is problematic because if you only have a few examples of common logic then you don’t really know what code is actually common and what features really need to be shared. Building too much into the abstraction can make the whole thing much worse to use and harder to change later.

Let’s look at a super simple example of what I mean. One area I’ve seen people go over the top on abstracting is handling of requests to other services with authentication, headers, CORS, etc. It’s not crazy to see someone take the following code and DRY it out.

function getAllBlogPosts(): Promise<BlogPost[]> {
    return fetch(
        "/posts",
        {
            mode: "cors",
            credentials: "same-origin",
            headers: {
                "content-type": "application/json",
            },
        },
    ).then((response) => response.json() as BlostPost[]);
}

function saveBlogPost(title: string, content: string): BlogPost {
    return fetch(
        "/posts",
        {
            method: "POST",
            body: JSON.stringify({ title, content }),
            mode: "cors",
            credentials: "same-origin",
            headers: {
                "content-type": "application/json",
            },
        },
    ).then((response) => response.json() as BlogPost);
}

You might see some of those fetch settings and right away think that there’s too much duplication and we should wrap that up.

function getAllBlogPosts(): Promise<BlogPost[]> {
    return sendRequest("/posts", "GET");
}

function saveBlogPost(title: string, content: string): BlogPost {
    return sendRequest("/posts", "POST", { title, content });
}

function sendRequest<R>(method: string, url: string, body?: unknown): Promise<R> {
    const options = {
        method,
        mode: "cors",
        credentials: "same-origin",
        headers: {
            "content-type": "application/json",
        },
    };

    if (body !== undefined) {
        options["body"] = JSON.stringify(body);
    }

    return fetch(url, body).then((response) => response.json() as R);
}

Seems like it’s better right? We’ve encapsulated all the CORS and credentials stuff and we have it automatically unmarshaling the response body as JSON! But a new requirement has come in and we need to make a new request that returns data as XML instead. Hmm I suppose we could make multiple methods for each data type we have to work with. But then the fetch stuff is duplicated so that’s no good. We could make the requester pass in a function to do the marshaling but then we have to add more arguments… We could require the caller to have to do all the marshaling and unmarshaling of data but then we end up with a very thin wrapper around fetch which doesn’t really do anything AND we have to duplicate all that marshaling logic everywhere!

This is one of those times where duplication is just going to be better. It’s fine if all the fetch calls look basically the same. Until you get to the point where all the fetch calls are littered all over the code base and you have a bunch of different teams working on the code or the duplicated code is getting really complicated or bug prone it’s easy enough to manage the duplication and adding abstractions is just going to cause more problems than it solves.

In fact, in some cases it can be easier to maintain duplicated code because you can really easily search for instances of it using something like ast-grep to find matching code and update it. It’s also easier to quickly understand what’s happening since you don’t have to jump around in the code and remember what’s happening where. It’s all in one place and you can just read it linearly. Finally by waiting until the whole picture of what the requirements are you can make sure that it’s actually abstracting away the right stuff and that it’ll deliver more value than copy/pasting.

Recommendations

My recommendations are pretty simple but also at least somewhat subjective.

  1. Don’t create an abstraction when you think there’s duplicate code until you have at least 3 examples of duplication.
  2. Don’t create abstractions just because of code duplication. Make sure that the abstraction actually makes things better.
  3. If you’re abstraction seems more complicated than the duplicated code then your abstraction might need to be split up or shouldn’t exist.
  4. If you think your existing abstractions are making it too hard to make changes then consider switching to WET code instead.
  5. Accept that code duplication doesn’t have to be a bad thing and in some cases will make things better.