Coincidental duplication costs really add up

When working in a team, different groups of people within that team have different concerns - developers, tester, designers, product, etc. These different concerns mean that different members of the team approach issues in ways that address their concerns. When working well, this approach allows people to specialise and ensure that a range of views is heard when discussing new functionality. However, it can also lead to a situation where the different groups assume that they have a shared mental mapping of how a piece of functionality should work when, in fact, there are unspoken differences there.

I've fallen in this trap a few times where I've made assumptions about how a new piece functionality will interact with the different parts of the app - those assumptions weren't shared. It's very easy to do this when that new functionality looks or behaves very similar to existing functionality. I call this Coincidental Duplication.

Coincidental Duplication is where we see common functionality between features that just happen to behave or look the same but were never intended to be connected. We spend time creating an abstraction to share that common functionality, only for that functionality to change in one of the features in a way that breaks that abstraction because to the designer, product lead, etc, that functionality was never connected, so they made no effort to change it consistently - the common functionality was just coincidental.

Falling into assumptions

For the most part examples of coincidental duplication are found around the edges of our projects: UI and API as these are the parts where we are interacting with other teams, and those different mental maps can lead to small breakdowns in communication. I want to look at a common example of coincidental duplication that we have probably all seen in our projects - a colour palette. We often create a colour palette at the start of a project as a way to consolidate the colours that we use in the project and allow us to change colours across the entire app by making one change.

The example below is an extension of UIColor that adds custom colours.

extension UIColor {

    //MARK: - ColorPalette

    class func brandBlue() -> UIColor {

        return UIColor(red: 51/255, green: 136/255, blue: 255/255, alpha: 1)
    }

    class func brandRed() -> UIColor {

        return UIColor(red: 255/255, green: 68/255, blue: 68/255, alpha: 1)
    }

    class func brandGreen() -> UIColor {

        return UIColor(red: 153/255, green: 204/255, blue: 0, alpha: 1)
    }

    class func brandGrey() -> UIColor {

        return UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1)
    }

    class func brandPurple() -> UIColor {

        return UIColor(red: 170/255, green: 102/255, blue: 255/255, alpha: 1)
    }
}

It's a nice short class with easy to understand consistent method names. It allows us to shortcut the colours that we use and prevents duplication in code. The trouble is that we didn't seek agreement that those are the only colours that the app will use. What ends up happening is that as the app grows, the designers want to add more subtle changes to better express their intentions in new feature.

So we end up with something like:

extension UIColor {

    //MARK: - Blue

    class func brandLightBlue() -> UIColor {

        return UIColor(red: 160/255, green: 192/255, blue: 255/255, alpha: 1)
    }

    class func brandBlue() -> UIColor {

        return UIColor(red: 51/255, green: 136/255, blue: 255/255, alpha: 1)
    }

    // MARK: - Red

    class func brandRed() -> UIColor {

        return UIColor(red: 255/255, green: 68/255, blue: 68/255, alpha: 1)
    }

    // MARK: - Green

    class func brandLightestGreen() -> UIColor {

        return UIColor(red: 238/255, green: 238/255, blue: 204/255, alpha: 1)
    }

    class func brandLightGreen() -> UIColor {

        return UIColor(red: 221/255, green: 238/255, blue: 153/255, alpha: 1)
    }

    class func brandGreen() -> UIColor {

        return UIColor(red: 153/255, green: 204/255, blue: 0, alpha: 1)
    }

    class func brandDarkGreen() -> UIColor {

        return UIColor(red: 68/255, green: 119/255, blue: 0, alpha: 1)
    }

    // MARK: - Grey

    class func brandLightGrey() -> UIColor {

        return UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
    }

    class func brandGrey() -> UIColor {

        return UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1)
    }

    class func brandDarkGrey() -> UIColor {

        return UIColor(red: 26/255, green: 26/255, blue: 26/255, alpha: 1)
    }

    // MARK: - Purple

    class func brandLightPurple() -> UIColor {

        return UIColor(red: 238/255, green: 224/255, blue: 238/255, alpha: 1)
    }

    class func brandPurple() -> UIColor {

        return UIColor(red: 170/255, green: 102/255, blue: 255/255, alpha: 1)
    }
}

In the above code snippet, we have added in a few more shades to our colours with the following scheme:

  • darkest
  • dark
  • standard
  • light
  • lightest

A slightly longer list of colours than before and some missing gaps between the different colours but still just about sticking to an abstract colour palette.

But the designers are still not brand 😡. They go on and use even more shades, all the time striving to create a better user experience - the evil bastards!

At this point, you can add in names like: lightDark, darkLight, etc. But one of the reasons for abstracting is to make things easier to read and reason about, does brandLightDarkPurple and brandDarkLightPurple do that? Rather than ploughing on and creating those new colour abstractions, we need to question if this colour palette concept we have been building is actually shared across the team?

Question your assumptions

"to assume is to make an ass out of u and me"

Any competent designer will endeavour to create a consistent UI/UX that allows the end-user to ground themselves as they move between different features. But breaking consistency between features can be a powerful tool in any designers toolbox and is often used to draw attention to a particular aspect of that feature. Before thinking about investing time in creating an abstraction based on someone else's work, we need to agree on what aspects of their work will stay consistent and which are free to change. If we silently assume that seemingly common elements are in fact, common when they are not, then all that we've ended up doing is:

  • Increasing initial development effort due to having to build the unnecessary abstraction.
  • Increasing maintenance effort as we attempt to hold the abstraction together given changing functionality.
  • Increasing deletion costs as we need to remember to delete the no longer wanted feature but how that deletion will affect any connected features via that shared abstraction.

As when those seemingly common elements change our abstractions can't handle those changes.

This increased work is the exact opposite of what we were attempting to achieve when we decided to abstract.

While it's common to share functionality between features, we need to make sure that what we are sharing is intentionally common. If that common functionality is intentional, we can then make smart abstraction choices. No-one likes to invest time building something elegant only for someone else to destroy it - save yourself heartache by avoiding coincidental duplication.

You can find the completed project with the colour example here.