As we’ve discussed in a previous blog post, product development is hypothesis testing. This is especially true in the early stages of a company when you need to confirm or reject your hypothesis as quickly as possible. This process is then repeated until you (hopefully) reach product-market fit. To get there, your team needs to be able to work and build at a pace that allows for this constant and rapid iteration.
Monolist is the command center for engineers — tasks, pull requests, messages, docs — all in one place. Learn more or try it for free.
Your company’s tech stack will immediately be a fundamental part of how your engineering team works and operates. This is even more important in a small company where the engineering team represents an outsize portion of the team as a whole.
Many people will recommend to early founders that they use the technologies they know and write in whatever language they're familiar with. Although this is good advice, like all other decisions it should be weighed carefully. While ideally you're fully familiar with the stack you choose in order to move as fast as you can, you should also be putting some thought into how these choices will affect you and your engineering team moving forward. For example, it may not always be best to pick the brand new (and unproven) technology on the block when you need to scale quickly and painlessly.
What to Optimize For
The overall goal can be summarized as this: minimize time spent on non-feature engineering work.
For every hour you or another engineer spend configuring, debugging, or learning a new system, that's an hour not spent iterating on the product to maximize learnings and value delivered to your users.
Here are the things to aim for when choosing your startup's early tech stack.
🤯 Easy to understand
On a small engineering team, everyone is going to be working everywhere in your codebase. Additionally, if things are going well, you'll be adding new engineers to the team fairly regularly. For these reasons, your codebase and systems should remain easy to understand and get started with. No one wants to spend multiple days banging their ahead against a spaghetti codebase or tangled dev environment.
There's a few things that can help with maintaining the grokkability:
Well-known languages or frameworks
Technologies often come and go quickly in software development. However, there are those libraries that are nearly ubiqitous. These are the languages and frameworks that the majority of software engineers would be at least vaguely familiar with. Examples would be Rails or React, both incredibly popular libraries used and supported by a large number of people and companies. This also means they have large and healthy ecosystems surrounding them.
Large refactors of core business logic are common when you're an early-stage company iterating often on your product direction. These are no longer nerve wracking when you always know what data you’re working with and when.
Self (or well) documented code
While the concept of true "self-documenting code" is fairly controversial, it can still be valuable when not taken to an extreme. Write and architect your code so that its purpose is clear and its execution path is easy to follow. Leave clarifying comments when blocks of code become overly complex out of necessity. As mentioned above, typed languages also help a lot here.
🙌 Our Recommendations
Rails — The simplicity of Rails' MVC model, coupled with the easy to read and write language Ruby, means it's easy for most engineers to become comfortable working in Rails and shipping production features. Rails also continues to power many well-known, large-scale applications such as Shopify and GitHub.
Caveat: Although Ruby is not typed by default, there is a new static type checker called Sorbet.
TypeScript — Defining your types and using TypeScript strictly results in code that’s easy to parse and safer to modify.
🔌 Plug and play
You should favor technologies that are easy to get up and running. On a small team without a dedicated devops team, engineers should not be dedicating time to setup complex and/or custom infrastructure. There are two things you should be on the lookout for:
A large community/ecosystem
If it even needs to be said, open-source is your friend. High numbers of users (downloads) and contributors are a good signal of a healthy ecosystem for an open-source library, meaning bugs should be resolved fairly quickly and new features will likely be shipped. A few quick (and imprecise) measures we use to quickly get a read:
The number of stars a repo has on GitHubThe date of the last significant updateThe number of outstanding (and/or stale) issues and pull requests.
This one is fairly self-explanatory. I provide the quotations because I've yet to find a library that is truly and literally setup-free for anything beyond the most basic use case. However, it is still good to aim for libraries that will not require a large amount of someone's time to configure.
None of this means, however, that you should always avoid the route that requires a little more work initially if it pays off in the long run. We'll talk more about that below in "Automate everything".
🙌 Our Recommendations
Rails — One of our picks again. The long history and large community around Rails means you can find a gem for practically anything your application could need to support. Many of them are even included in Rails itself.
Next.js — Next brings a Rails-like simplicity to handling an isomorphic React application. Data fetching and page definitions are unified, while you get to continue using any React patterns or components you prefer.
🔄 Aim for reusability
Every time you re-use a block of code, you're saving some fraction of the time originally spent writing and testing it. It also reduces the surface area for bugs and for engineers to learn about in your codebase. Here are some ways to aim for reusability across your application's codebase.
Share languages or frameworks
When multiple parts of your system are using the same core language or framework, it becomes increasingly advantageous to build modularly. While this always helps with separation of concerns and unit testing, it can now open up new ways to share and re-use code across your application.
For example, maintaining and publishing all of your app state across multiple clients becomes much more manageable when you can use the exact same Redux code across web, iOS, and Android.
There are limits to reusability, however, as complexity often grows exponentially in the same direction. It is important to keep that in mind as you still aim to achieve the first goal we discussed, "easy to understand".
🙌 Our Recommendations
React Native — RN is one of those platforms that encourages code re-use with other React applications. Write your native mobile applications (iOS, Android) using your same modules in familiar architectures.
Gatsby — Another React-related library, Gatsby allows for easy creation and publishing of static websites using the same React files and patterns. Great for maintaining a scalable marketing site.
Lerna — Lerna helps to more easily maintain a monorepo of inter-dependent JS packages. Easily bump while keeping versions in sync, as well as publish to NPM.
🛠 Automate everything
As we discussed above, complex systems and configurations should be avoided whenever possible. However, there's one exception: automation.
Likely the place best suited for automation is your CI/CD process. This should include everything from linting, to testing, to production deployment. Ideally it's happening quite often, but it can also include any number of steps each of varying complexity.
Automating any task is going to require a varying amount of upfront effort. However, the ongoing time saved is a multiple of the frequency and duration of these tasks. Additionally, the ongoing bugs and confusion that would arise from doing every process manually are immeasurable.
Terraform — Terraform helps us to fully manage our cloud infrastructure in source-controlled and peer-reviewed code. This allows for consistency and self-documentation, while giving us much more confidence in our deployed systems.
Docker — Docker allows us to pre-define our various service environments (we have around 12) as reproducible and deployable containers. Again this allows for consistency, self-documentation, and confidence in our systems.
❗️ Are you a software engineer?
At Monolist, we're following these tips to rapidly ship new features that help software engineers be their most productive. If you want to try it for free, just click here.