< Articles


Not All Code Should Be Dry

DRY, or Don't Repeat Yourself, is one of the guiding principles of every software project. Whether it be code or infrastructure, we're at our peak as engineers when each problem only needs to be solved once.

It's not uncommon though for this virtue to become a vice. If we're not careful, a unified solution for diverging use cases may introduce unneeded complexity and complexity tends to have an exponential relationship with the time, cost, and defect count for a given project.

This principle has been at the forefront of my mind after our December product release. As I've mentioned in previous articles, I manage two frontend applications for Xactware: a product for estimating property claims and an adminstrative product that manages company preferences.

Initially, there wasn't much feature overlap between the two products. This changed when we received designs for a feature that would be identical in both the admin product and in the client product. At the admin level, it would set company-wide claim defaults. Users could then override some of these options at the client level, setting up claim defaults for their own claims.

The feature itself was huge. We were looking at ten expansion cards, 20+ dialogs, and a number of autocomplete fields. Additionally, the state logic was pretty complicated. Aside from autosave all debounced changes, we also needed to manage passing data between up to 4 layers of dialogs.

Given the size of the feature, we considered a number of options so as to only write the code once.

Potential DRY Solutions

For a brief moment, we floated the idea of adding pieces of the feature to our components library. It wouldn't require any additional build setup and could be easily consumed by the two applications. While it would have been the simplest of our potential options, we decided that our components library shoudn't be aware of domain-specific models.

The other option was to create a separate project. But that would involve a lot of extra work. A new repository, setting up and maintaining repository permissions, bundling with rollup or webpack to make the library consumable, and an additional TeamCity project.

Given the two options, the software engineer in me wanted to go down the separate repository path. I hate seeing potentially wasted work in the pipeline, especially with how aggressive our goals have been over the last year and a half. But the business man inside of me told me to wait. Yes, a little bit of waste was possible but something about the way these projects were set up led me to believe that they would quickly diverge in behavior.

Straw That Broke the Camel's Back

Fast forward to the mid December and my suspicions were confirmed. At this point, we had been maintaining two isolated source code blocks for a few months.

After deploying a completely separate feature, we realized that our saving strategy between the administrative application and the online application needed to be different. Up to that point, we had only employed "Save" buttons to the extent that they were necessary (dialogs, drawers) but for the most part, we'd structured the code to autosave all debounced changes.

While it had been a great strategy in our client application, it didn't bode well for the admin application. Changes at that level have a much larger impact (i.e. company wide vs. user only) and we felt that changes in the admin application should be more explicit. Instead of autosaving, we would instead stage changes on the frontend and have the user click Save. Additionally, we would pop up confirmation dialogs as an added layer of protection from company wide mistakes.

This seemingly simple paradigm shift will drastically alter the code in our admininstrative version of the feature. We've officially crossed the threshold of a shared location saving us work. They're now different features entirely.

Takeaways

This experience reminded me of the importance of predicting future business needs as we develop features. Everyone involved in a software project (engineers, designers, products groups, project managers) needs to look at the feature at hand within the context of the long-term vision. Sometimes we'll get it wrong and over-engineer a feature. Other times, our initial implementation won't be flexible enough. But just having the future in mind will get us closer to the goal of having no wasted work.

Here's what I would recommend so as to avoid the over-engineering trap:

First, consider the relative risk and reward of the potential solutions before you start. In our case, the risk of keeping the code separate in both applications was low. If we ended up undoing that solution and moving to a shared location, the only wasted work would be copy/pasting the code into another location and fixing defects and making improvements in two locations for a few months.

On the other hand, the risk of having the code in a shared location and then later moving to isolated locations was much higher. The wasted work in that scenario would have been a new build system and repository setup as well as unwinding the additional logic we would have needed for our shared solution.

Second, order is everything in software engineering and you have to consider the big picture of the product when handling priorities. In our case, we decided to not do any work in the admin application until we felt the feature in the client application was perfect. This would allow us to just copy/paste the solution over to the client application and prevent as much "double work" as possible.

It can be tough to think long term in large corporations, especially with aggressive deadlines. It's tempting to try scoring political points instead of maintaining a long-term view. But drawing that line in the sand that we wouldn't do anything in the admin application until the client application was pretty much done saved us a ton of work and we were still able to hit our deadlines.

Lastly, I think there's an emotional intelligence element to predicting how product expectations will change. As an engineer, you have to get a feel for how confident the group is in a given solution. Part of this is based on the team's track record (i.e. how often have things changed after the group was confident they wouldn't) but there's also an element of intuition.

If you're unsure, it's best to error on the side of flexibility before implementing a rigid DRY solution. It's best to hedge your bets, even if the absolute amount of work is greater than the value delivered to the customer.