We are natural problem solvers. And that's no wonder - problem-solving is really rewarding. It gives you a sense of achievement and for many developers is the single most important thing that keeps us working. Especially when the problems you deal with are challenging and meaningful to you.
But precisely because of these reasons, it might be easy to become not only a problem solver but a problem seeker. Kent C. Dodds has an excellent blog post about this.
Some time ago I posted this tweet, based on one discussion we had with our team at work.
Looking at this you probably think - what a terrible piece of advice. And in most cases, you'd be right! But I'd argue this actually holds up in some situations. Let me tell you a story.
In the project, I'm currently working on we are developing an enterprise application for cloud storage, content collaboration, and data governance. There are multiple standalone front-end applications, each supporting a different part of the system. There's also a single shared library with UI components so that our apps look consistent. This library is a dependency of each of the individual applications.
This shared library contains simple UI components such as a button, dropdown, or form fields. These components are typically used in multiple applications.
Recently we've discussed whether we should move the implementation of one of the components to shared library - namely a date picker.
Currently, the situation looks like this. We have one date picker implementation in one of the apps and another implementation in the other. In the first app, the component is heavily used, in the second it's used very rarely.
So sure, it seems that it would make sense to move the first implementation to shared and reuse in both apps. Less code and visual consistency - checked. Technically, it also should allow us to move faster in the future if we need to implement a view with this component in any of the other apps. Only benefits, right?
Unpacking all the problems
On the surface, it seems straightforward - move one implementation and adjust the places that use the component. But when we looked closer we found out the following.
The component has a bunch of user-visible text in a bunch of variants, depending on the component's state. Currently, the shared library does not contain any library for translations, so we should consider either adding one (which is a big task) or create some abstraction that would allow getting all the translations to the component (which increases complexity).
The next element is date manipulation. The component internally does quite a bit of work around dates, calculating date ranges and formatting. The first problem is that one application uses
moment, the other uses
dayjs. Also, similar to internationalization, the shared library does not contain any date manipulation library - yet again, we have a couple of options here.
- Use the native inbuilt
Dateobject. I don't know about you, but to me, it's typically asking for trouble in the long run.
- Install a library. Then the question becomes - should we keep using
momentwhich is pretty large, or use
dayjs, which is lighter. Either of these options would make each app have two, similar date libraries (unless you're planning on refactoring one of the applications to use a different date library - good luck).
- Use inversion of control and allow the clients (applications) to format dates from outside, which might increase the complexity.
Currently, the date picker component looks slightly different in the first and second application. Using a single component would definitely improve the consistency.
But looking at where exactly the date picker is used in the second application it turns out it's used in exactly one place. The screen on which it can be seen corresponds to roughly 0.1% of the total views of the second app. On top of that, the second app is used far less often by the users.
Additionally, looking at the roadmap we have we don't expect adding a date picker to the second or third app in any foreseeable future. Is it worth solving to cure the inconsistency that appears for a fraction of a percent of the total views of the system?
We've outlined a couple of issues that we will hit during the unification process. Doing that, we've realized that it makes very little sense to make the move right now. Sure it would be great to have this component unified and ready to use. But the number of issues we'd need to address and the lack of clear return on the investment for the nearest future made us decide that it isn't the time to make the move.
Don't get me wrong. I'm a firm advocate of incremental refactoring. Improving the code you're working around so that it's a little better than it was before is a good thing. I even wrote about it quite a bit in this blog post. What's important is the ability to choose which problems you're going to solve and which not (at least not yet).
Ultimately we decided to not address the problem we faced. At least not for now. There were just not enough benefits compared to the cost. There are also other areas we could focus on right now.
Here are the questions you might want to ask yourself the next time.
- What problems do I need to solve along the way? What is the cost of solving the problem?
- What is the cost of maintaining the solution to the problem I come up with?
- What is the return on the investment of solving the problem?
- Do I have other problems with a higher return on investment I could be addressing now?
Once you have the answers, decide - should you solve your problem or move on. Sometimes not solving the problem is actually the best option. Whatever you decide, good luck!