Over the past two decades, I’ve worked as a software developer, tech lead, and CTO at various startups, including my own. I’ve accumulated a number of principles I use to make major technical decisions. Most of the principles outlined here were learned the hard way. You might not fully agree with all of the principles, but I hope at least some of them could help those who struggle with tough technical decisions.


Focus on your core business

Before implementing a new functionality, the very first question you should ask is whether that functionality will be a part of your company’s core business. If the answer is no, you should look for a third-party library or tool, preferably open-source. If the available options on the market don’t quite solve your needs, then you might consider implementing the functionality yourself, but only after doing a careful cost-benefit analysis. Whenever possible, avoid writing new code – the best code is no code.

When I first joined Faradai, the company had the first version of its energy management software platform up and running, successfully serving customers. The platform generally worked fine, but AML, its custom web frontend framework, was hard to use and extend.

Since no other company used AML, there weren’t any outside developers exposed to it, so we had to train new developers before they became productive in it. Worse, some of the developers complained that their newly learned AML skills wouldn’t be useful elsewhere, and you know what, they are right!

In a startup, time to market is critical, and training new developers slowed us down. Had we used a popular frontend framework, we could have simply hired React or Vue developers and had them write code from day one.

Finally, when our AML framework couldn’t do something we needed, we had to stop working on the core application code and work on AML instead. As a software company operating in the carbon accounting and energy management domains, maintaining a custom frontend framework was clearly not our core business.


No rewrites unless absolutely necessary

One of the first major technical decisions I made at Faradai was to switch from AML, our custom web frontend framework, to Vue. However, there was a lot of code written in AML, so the decision to rewrite the frontend in Vue wasn’t made lightly as explained above. Since the rewrite would take some time, we did the bare-minimum to maintain AML until we reached feature parity in the Vue version, which also added features like dashboard widgets that could be dragged and dropped anywhere on the screen, something AML lacked.

Although we had to rewrite the frontend code, the backend stayed mostly as it was before because the code was solid and needed just a bit of refactoring and performance tweaks.

Rewriting large sections of code is highly risky. Not only is it easy to break existing functionality, but it wastes precious time that could have been used to add new features users need.


Have a simple tech stack

We selected Vue over React because, for us, Vue was simpler to learn and use. There were other, even simpler web frontend frameworks, but they didn’t quite meet our needs. As Einstein had once said, “Everything should be made as simple as possible, but no simpler,” it’s best to avoid introducing unnecessary complexity since it might come back to bite you in the future in the form of high maintenance and financial costs.

When in doubt, choose a monolith over a bunch of microservices, a load balanced application server over a Kubernetes cluster, PostgreSQL over the database flavor of the day, and so on. If you ever start to outgrow your current tech stack, you can always add more complexity later, but you cannot always simplify an overly complex tech stack.


Minimize your dependencies

In today’s highly connected world, software applications don’t exist in a vacuum. There can be a vast number of dependencies as far as the eye can see. Just looking at the dependency tree of a typical Node.js application can reveal tens of primary dependencies, with each dependency having its own dependencies and those dependencies having their own, ad infinitum. If you count them all, you can easily end up with hundreds or even thousands of dependencies in total, and this is just the code side. The dependency trees on the server side can be even deeper if you go all the way down to kernel level.

With so many dependencies, it’s a miracle that anything works. As software developers, we really are standing on the shoulders of millions of software developers.

Since it’s next to impossible to have control over your entire dependency tree, the only thing that you can realistically control is your primary dependencies, and it’s a good idea to keep them as few as possible. Statistically speaking, the more dependencies you have, the greater the chances of something going wrong.


Avoid scope creep

Software is infinitely malleable, and there’s always the temptation of adding one more feature. Don’t, under any circumstances, accept change requests if the previously agreed-upon timeline for development would stay the same.

Many software developers are eternal optimists, a good trait that’s necessary to deal with a seemingly unending string of technical challenges, but it can also be our undoing. Software development is a complex process, and there are oftentimes previously unrealized technical issues that need to be addressed before implementing a feature. There’s a good chance that a seemingly minor change may require more time than originally thought.

Finally, don’t fall into the trap of non-technical people trying to convince you with shaky arguments like “How hard could it be? App X already has that feature.” The truth is they don’t have the technical background to speculate about the difficulty of technical problems. However, you still need to explain the technical aspects to them in plain language. A curt “No, that’s not possible.” won’t suffice.


Prioritize by taking business realities into account

There are many methods to choose for prioritizing the items in your roadmap. It’s not critically important which one you use, but you should pick one and stick with it. I personally like Cost of Delay because it focuses on the financial impact of feature requests – a seemingly important feature may easily turn out to be not so important for your company from a financial perspective.

Jim Rohn said, “Time is our most valuable asset, yet we tend to waste it, kill it, and spend it rather than invest it.” Companies need a steady cash flow to stay healthy, and implementing software features that don’t bring in revenue is not only a waste of time but also incurs a significant opportunity cost.


Build prototypes before making risky decisions

Some of the decisions you make may involve a lot of unknowns that need to be explored before making a final decision. In cases like that, it pays to do some exploratory work before fully committing to a plan of action.

For example, digitizing energy bills is fraught with risks. It’s all too easy to get the numbers wrong. We weren’t comfortable with the potentially high error rate. So, we explored various technologies. OCR was the obvious choice, but it performed poorly in our experiments.

Switching to large language models improved reliability significantly, but if we hadn’t tried exploring different technologies and built prototypes, we might have made the error of committing to using OCR, costing us a great deal of time and money.


Work in cross-functional teams

A successful software application isn’t just written – it’s the joint effort of a cross-functional team of product managers, UX designers, software developers, and QA engineers.

If a UX designer creates a screen design that would take twice as long to implement as a more traditional design, the software developers can offer some revisions for a faster turnaround as long as the original design isn’t compromised much.

Similarly, if the scope of a feature as outlined by the product manager is too complex to implement in a reasonable amount of time, the scope can be streamlined according to the feedback of the rest of the team.

Finally, if the software developers say they need to pay off some of the accumulated technical depth by doing some refactoring work, the development schedule can be revised to allow time for the refactoring.


Trust your team

This is the most important principle by far. If you can’t trust your team to make the necessary day-to-day technical decisions, then you will be quickly overwhelmed and your team will likely be demoralized as well. As the Russian proverb says, “Trust, but verify”.


Related: