Developing great software is about making great decisions. Here are some of the tradeoffs that we face every day.
Latest and greatest vs. Tried and tested
When faced with a new problem, we're often in a situation where we could use a tool that we know well, or use something new. This might be a programming language, library, framework or platform.
With familiarity comes speed. You can bypass the learning phase and jump straight into implementation.
The problem is, if you always take this approach, you will never learn anything new. You might become so tied to your favourite tool that you lose perspective. You risk becoming so embedded in your methods that subconscious biases start building up against other alternatives.
As soon as you start looking around at alternatives, you will find there is always something newer, shinier, with a loyal group of evangelists writing blogs like 'Why are people still using [the thing you're still using] in [the year you're still using it]?'. Should you leap and learn the new thing?
Learning new tools comes with significant overhead. There will be a period when you're a beginner and you will make beginner mistakes. Spending this time might be worth it. If it is a better tool for the job, it might even be quicker.
There might be other benefits. Dipping your toe into another tool brings with it a community of different techniques and practices. Often, these techniques apply to other domains. I started using Flexbox in C++ after working with React Native.
Choosing between old and new is a difficult balance to strike that only comes with experience. There are good and bad reasons for sticking to what you know and learning something new so you should make each decision on a case-by-case basis.
New dependency vs. Roll your own
You need some new functionality. Should you bring in someone else's code, or re-implement it yourself?
Often, the answer to this is easy. If the functionality is significant or critical enough, you will always bring in a third-party dependency. If the functionality is trivial, you will probably write it yourself.
The tricky area is in between.
As developers, we are conditioned to avoid duplication. So, we should always bring in a third-party dependency, right?
Unfortunately, it's not that simple.
Dependencies come with dependencies. This leads the number of dependencies in a project to increase (what feels like) super-exponentially. It doesn't take many of these to go stale before you're in dependency hell.
Although a dependency may solve a problem, it might not solve it in the way you would have. It might be solving for a more general case. It might use a different coding style. It might need an adapter layer to work the way you want it to. The priorities of the maintainers might be different from yours.
Each new dependency should be carefully considered in the context of the current project.
Design for the future vs. Deliver now
The amount of future-proofing to add to our code is a delicate balance.
If we decide to future-proof our code and the extra functionality is never required, we've unnecessarily gold plated our code. If we decide to future-proof our code and the extra functionality is required, we have a well-engineered system.
If we opt for a lightweight solution and extra functionality is never required, we have a lean system. If we opt for a lightweight solution and extra functionality is required, we have to rework or extend our system.
This is a simplification; there are other factors to consider, too.
How difficult would it be to rework a lightweight solution? If it's simple to rework, there is little benefit in a more heavyweight solution.
How much more difficult is the future-proofing? If it's simple to future-proof, it's probably worth it.
How much do we learn from a lightweight solution? If we can get value from developing quickly, we might learn that we never need the heavyweight solution.
How likely is it that this is going to change? If we can see changes in the short-term, it's more likely that we will need the extra future-proofing.
Learning by reading vs. Learning by doing
When I'm learning something new, there usually comes a point when I'm ready to break away from documentation and start using it for real. Finding this point is an important tradeoff.
If I jump too early, I risk missing important information. Some things are difficult to learn by making mistakes. You simply need to be told.
If I jump too late, I risk wasting a lot of time learning things that might be irrelevant. I risk spending a lot of time reading things won't make sense until I have some practical knowledge.
If I get it right, I know enough to get started. I can then start to apply it to my specific problem. I know the core concepts and I can revisit the documentation if I have a problem later down the line.
Fix it vs. Leave it alone
We often encounter code that we wish we could improve - whether it's our code or someone else's. We have to ask ourselves the question, "shall I fix it, or leave it alone?".
Part of being a good developer is following the boy scout rule: leave the code better than you found it.
So we identify an improvement, we make the change, and we get a warm, fuzzy feeling inside. We just made something better. We reduced technical debt.
Except, for every change, there is a chance of introducing a new bug.
There is also the extra burden that you're placing on reviewers of your code. They now need to unpick the changes you intended to make from the "while I was in there" changes.
For every non-functional code improvement that we make, we should be asking ourselves some important questions: How much of an improvement is this change? What are the risks of making this change? Does it make sense to make this change now, or should I make a note and deal with it later?
The quality of a developer isn't how much they know - almost all developers are smart and can pick things up quickly. The quality of a developer is how effectively they make decisions.
The best developers don't do the same thing every time, they constantly evaluate every decision and tradeoff they make. They are balanced and reflective, rather than dogmatic and stubborn. They recognise when they've made a wrong choice and recalibrate for future decisions.