Zum is a great example of a typical Silicon Valley startup. The company began with a bold vision and created a working product within a very short time and raised $5.5M Series A from Sequoia in 2017, then $19M in 2018, and $40M in 2019. All this time, execution speed was an integral part of the force pushing the company to new heights. We realized early that building new features co-exists with the constant improvement of existing code.
In the last year, we have made significant changes to what we build and how we build new features. This included a complete overhaul of our platform infrastructure. We will be writing about our experiences and how our platform has evolved in a series of posts in our blog.
At the beginning (end of 2018), our project was a single npm package written in ES5 syntax. The project structure looked similar to most NodeJS applications: API routers, controllers with business logic, models with database logic.
As the codebase grew, and so did the team, we noticed 2 things:
- We started to face runtime bugs more and more often - be it an undefined variable or a missing import
- Development speed decreased since we started to be afraid of modifying the existing code
Why did it happen? Writing the initial first code is simple. An engineer usually tests it works perfectly as they develop a feature. However, the next person who changes the same file later (by adding other features or modifying an existing one) can't be relied on to re-test all the affected code in the file. Unit tests do not completely solve this issue either. We effectively may end up leaving it up to a real user to stumble upon a bug at the runtime.
What did we get out of using static typing? Startups are all about execution and, most important - fast execution. Hence, all improvements should be directly connected to the value for the business right away.
Catching most of the trivial errors at the time of packaging a project without a need to have a 100% unit-test coverage - is one of the immediate benefits. Our code doesn't break in runtime just because of a simple typo in a function name.
Maybe an even bigger advantage of the types is moving to the mode where things have a name. Speed of development increased dramatically, especially for the features which required changes in the existing code. Why? It's much faster to understand the function when you only need to read the typed interface. Even better, when the interface is defined with reusable types from the modules you are already familiar with.
"…the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write."
- Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship"
Things move faster also because we are not afraid to change the code anymore. Types make sure code still "compiles" after the change, and with IDEs like WebStorm - refactoring and renaming are automated. Even things like intelligent code completion - may not directly speed up the development, but as engineers, we enjoy it, which brings down the fatigue of writing boilerplate. Eventually, we write more and better since we focus on the logic, not the spelling of variables and methods.
How did we do it? First, our team spent around 3 full days reading cover-to-cover the TypeScript Handbook. It was a bit faster for those who came from typed languages like Java and C#, although it required some mental re-mapping of a few concepts since things are not identical (e.g., duck typing).
As noted before, initially, all the code was in a single package (let's call it "legacy"). For all new code we started to create separate npm packages in pure TypeScript and import them to the "legacy" package. This became possible by moving to a multi-package monorepo with Yarn Workspaces (follow our blog to stay tuned for a separate post on this topic).
If the new code did not deserve a separate package - it was created in the "legacy" one, but as a new TS file (even if it's just a single function). Sometimes it's not possible due to circular dependency between files. The last resort is renaming from .js to .ts, which comes with a price - you either have to fix all type errors immediately or set up separate, more forgiving, compilation rules of such files (more on it here and here).
It allowed us to migrate gradually and avoid full code refactoring. This way, types are defined for the functionality which is actually used/current - no need to spend effort on a dead or rarely used code.
- We develop features faster and ship them more often (deploying almost every day vs. once a week in the past)
- Engineers are happy to use new technologies and work with a more readable code
- Backend (NodeJS) and Frontend (Angular 11) codebases align well, allowing us to build common libraries
- Users and the support team are happy, too - much fewer silly bugs in production
Interested in working with us? Please ping me at firstname.lastname@example.org