Thinking tradeoffs: Why there is no such thing as a perfect tech stack


Dawid Mazur

Spis treści

Over the years of my career as a software engineer, I have seen many projects at various stages of development. Most of them were mature or even legacy projects where I experienced first-hand the consequences of the technical decisions made by my fellow programmers. As we all know, things are much clearer in hindsight. Poor choices made by people that were working on the project before me baffled me often. And I vowed not to make the same mistakes myself. I even gave talks, trying to convince my colleagues to give up greenfield projects for some time and experience legacy code themselves, so they can make the same conclusions and learn.

A quick Google search told me that the average job tenure of a software engineer is about four years. In my bubble, I rarely see even that much. This means that in this industry, when we make big technical decisions, we don’t tend to stay long enough to see their consequences. That was the same for me as well until I started working at Clearcode and stuck to one project for almost five years.

Being in that position allowed me to do a self-reflection and rethink the approach to choosing technology, architecture and approach in software engineering. Do you know what is the biggest technical decision of them all? Choosing the right tech stack at the beginning of the project. This might be the single most defining moment to determine the future of most IT products. Contrary to the tone of this article so far, we did a pretty good job as a team. Did it contribute to the project being successful and our work being easier in the future? It certainly did, especially compared to some of my other gigs. Did we avoid problems and changes, though? Of course not.

Again, in hindsight, we could make some better choices for sure. So I’ve sat and thought: what would make our tech stack perfect? Was it perfect at the moment? What could we have done differently to arrive at “perfect” today? Let’s start by asking ourselves:

What makes a tech stack?

Many sources online define this by example. LAMP is a tech stack, and so is MEAN. But choosing a programming language, database or a framework is only the beginning. There are many choices to be made at the start that are as significant:

  • infrastructure providers like AWS, Azure, VPSes,
  • provisioning and virtualization software,
  • 3rd party integrations, for example, Stripe, Hubspot, Localize,
  • low/no-code solutions,
  • tools, like Atlassian, GitHub, IDEs, CI, testing software,
  • standards, like for APIs: REST, GraphQL, HATEOAS,
  • architecture patterns, monoliths, microservices, event sourcing,
  • hardware, target platform, devices,
  • design, accessibility, internationalization,
  • probably more.

Given all that ,I would define a tech stack as a collection of tools, standards, patterns, and design choices that make the project. They’re not all equally important or even present in some projects, of course, but you generally do not want to miss a crucial part. You usually get one shot at getting it right, for some of these.

A perfect fit

So, how does one get this right? Technically, for every project, there is a combination of variables listed above that is the best choice. You just have to find the right one. There are many sources online that can aid you in finding the mythical Perfect Fit, but one cannot find a fit without asking: Who’s in the changing room?

When you think about it, there are many stakeholders, so we’re looking for something that perfectly fits:

  • the business requirements,
  • the time and budget constraints,
  • the end user,
  • the development team,
  • the dev hiring market, now and in the future,
  • the target market, current and hopefully the one we’ll be expanding towards.

It quickly becomes apparent that one size does not fit all, in software development. Something will have to give and some stakeholders will have to agree to some discomforts. While it’s easy to look for pros when choosing technological solutions, we often forget to look at whose comfort we will have to sacrifice or make up for as a result of that choice.

Fashion and biases

Unfortunately, as technical professionals, we notoriously forget about business and end users and focus on technical challenges first. This bias is something that I try to constantly remind myself about. In the end, we’re not on the receiving end of the product we’re working on. Kent Beck said it best:

“Software development that doesn’t acknowledge economics
risks the hollow victory of a ‘technical success’.”

It’s easy to get excited about technology. We’re drawn to new tools, the possibilities they present, and the problems they solve. As people passionate about software engineering we get new toys every day and often for free! Even worse, we tend to be the worst fashion victims of them all, just waiting to toss aside the old and boring tech to embrace the newest thing. We’re only human, we have our biases and trends, and that’s to be expected. What’s not great is when the “victim” of that fashion is either the client or the end user.

It doesn’t help that the new stuff often gets pushed as a silver bullet, a logical next step, a cure for all that frustrates us every day with our imperfect, old tech, full of quirks and design flaws. It never occurs to us that we know of these problems because we’ve had time to take a closer look.

Thinking tradeoffs

Like our tendency to forget business needs, this is another bias we have to be constantly aware of. What I’m suggesting is that instead of thinking about the advantages of a particular tech choice, we should look at tradeoffs first. Do not think Pros and Cons. Think Cons and then Pros.

By now, it should be obvious that there will always be tradeoffs. We just have to focus on those + try to think about other stakeholders. Some of the things we can live with, some are deal breakers, and if we’re able to recognize them, we will save ourselves a lot of pain in the future.

A simple example from the past is CQRS or Event Sourcing. A few years ago, it seemed like these patterns would solve all software engineering. I’ve read a ton of enthusiastic blog posts and articles, and because that was a hot topic, it seemed like everyone was using it. Only once in a while, someone mentioned the compromises one has to make to use these in a project, like a huge code and infrastructure overhead, a danger of improper implementation, or the fact that they’re only a good idea in large-scale projects.

A not-so-simple example is TypeScript. When you’ve been using Vanilla JS for some time, switching to TypeScript seems to be a no-brainer. TS is far from perfect, of course, but arguing for sticking to just JavaScript is more often than not a losing battle. But what if we think about the cons first?

  • TypeScript adds some code overhead and requires more tooling to use, which is maybe too much if we’re just writing a function or a small service,
  • it can provide a false sense of security, especially for developers who have little experience or knowledge of the language, in which case they can overly rely on the typing system,
  • it doesn’t always play well with external libraries, creating a need for some acrobatics if we want to use types effectively,
  • it’s created and maintained by Microsoft, and while it is open source and fairly responsive to the community now, there is still a risk of it ending up like Java with its many branches and complicated licencing, not to mention that Microsoft doesn’t have a great history when it comes to playing fair and respecting standards,
  • with the current trend of ECMAScript being actively modernized and updated, TS can quickly become obsolete or even conflict with ES, when it implements the mechanisms that are present in TS already,
  • remember CoffeeScript? given the previous point, there is a chance that TS will be forgotten as well. This may lead to rewriting thousands of lines of code (which I’ve experienced first-hand, and it was ugly) + hiring people that have experience and still want to work with tech like that can be a nightmare.

Given all that, it’s good to ask ourselves: do the advantages outweigh the disadvantages? Are we, by using TypeScript, gaining that much value that it’s worth the risk of potentially dealing with these issues in 5 years?

Are we all doomed?

Does that all mean that when planning a project there is no place for innovation? New technologies and trying things out? Of course not. But a decision to go with something fairly new has to be a well-informed one. That means understanding risks and weighing them against what we gain.

Starting a new project is exciting, usually, we’re full of ideas and are anxious to start implementing them. This often leads to making rash decisions. If we want to be successful, we need to exercise some restraint and make sure that we really understand the product, the business requirements, and the target market. Having a small, healthy dose of pessimism helps as well.

Would my 5-year project benefit from that type of thinking? Probably. Would it allow us to avoid all future problems? I don’t think so. There are no perfect answers, just better ones. Engineering is hard, if it was simple, our job would be boring.

The only solution is to grow as professionals, educate ourselves, discuss and most importantly: retrospect on our experiences and learn from them. Learning is always better with a community, so check out our IT Depends meetup, where we ask hard questions, explore ideas and share our adventures in software engineering, and much more!


Sprawdź, kim jesteśmy i jak się
u nas pracuje