« Back to home

Which framework should you choose so that it doesn't hold you back few years later, when your business starts getting traction?

What later became our startup, DeskMe, started as a hackathon project. So the first version was done in 2 days, and it was written in Meteor.js. Three years later, I understood that we need to jump off the Meteor.js train, because the framework was no longer fitting our needs. Two years after that, we're still struggling to get rid of it 😁

I saw similar situation in other startups, including very successful unicorns: it takes years and years, and thousands of man-hours, to revert the wrong choices, that are made so lightly in the beginning. So, choose your framework wisely. Or at least, try.

How needs of a startup change over time

Let's go through the stages of a typical webdev startup.

MVP

In order to ignite a startup, you need some kind of a prototype. At this stage, it is very inefficient to use low-level frameworks and languages: you get a lot of control, but it takes too much time to create something presentable. Also, whatever low level optimizations you will build, they are:

  1. not really needed as you don't have many users or data at this stage
  2. likely to end up in the trash bin anyways due to rewrites during pivoting.

So at this stage, the perfect strategy would be to use a really high level framework that does everything for you. Something like Next or Meteor or Angular or similar.

Pivoting

Second stage of a startup - pivoting. You thought you had a good idea, but that's not what the customers want, as it turns out. And there it all starts: one rewrite, second rewrite, etc. Huge amount of changes in the codebase. How do you keep up with this?

The thing I found that worked really well for me - full intellisense coverage. Good tests coverage would have been even better, definitely go for it if you can afford it, but writing and rewriting tests (remember, we rewrite a lot during this stage!) does take time, and your application at this point is likely really simple and it's much easier to test main flows manually every time.

Full intellisense coverage would often mean saying "no" to HTML templates. For example, at DeskMe, we moved from Vue HTML templates to JSX, even though Vue templates are better - they're more readable, more concise, etc. - but not covered by intellisense... I had to migrate about 2000 lines of templates to JSX, this is exactly why I created the Vue2Jsx utility. Word of caution though: even though this helped us a lot, but at least in Vue 2.0 world, JSX templates are barely used by the community. As a result, you'll end up working out a lot of stuff on your own, because there are no existing solutions available. For example, I had to patch Typescript in order to make Vue JSX compile with TS, and even worse, this patch had not been accepted mainstream as they didn't want to tailor their code for specific frameworks (except React 😅).

Full intellisense coverage also means no magic strings, and this means no hardcoding of API endpoint URLs, which can usually be solved with some code generation + CI, e.g. by using protobuf or similar. Turns out, you can go even further with that if you have monorepo and use TypeScript both on frontend and backend. So what I did, I created a seamless code navigation experience that spans across the frontend/backend boundary: you can basically press F12 and jump from frontend code right into the backend, you can find all references across frontend and backend, you can rename stuff, etc. I described this solution in detail on HelsinkiJS few years back: 📺 Full-stack intellisense.

Full intellisense also means no magic strings when working with database. So you need some kind of typed ORM for SQL (although there aren't any really good ones in my opinion), or you could use MongoDB. Funny thing, although Meteor.js does use MongoDB, the corresponding type definitions haven't been created back then. Guess who wrote them 😉...

So yes, we did solve all these challenges, but as you can see, it required quite some additional time and effort, which I would better spend on the startup itself, on the business logic and features...

So if we selected some framework with full intellisense coverage on the first stage, we would have saved a lot of time. A LOT.

Age of optimizations

Finally comes the time when your startup has got some load and some data. Most likely, at first it gonna be data. Not much yet, in the range of tens of thousands rows, but still, it is already unfeasible to keep all this data on the frontend, you will need to start thinking about paging and search, you will need to start adding indexes to the database, caching here and there, you will start optimizing a little bit, and then more and more, as amount of data and users grows.

Roughly at the same time, your frontend bundle is likely to grow outside of reasonable size, because after all these pivots, you've got a lot of functionality (not all of it used, but you never know 😅), so you would need to start doing things like code splitting, etc.

And now is exactly the time when you will want that low level control, possibility to easily dissect any part of the framework and rewrite it from scratch to optimize it specifically for your company needs. And if the framework is not flexible enough, you're in trouble.

This is the time when frameworks like React start shining, since usually with React, you end up with some kind of a "our React stack", which is a collection of small libraries, every one of them has it's own specific purpose, and every one of them is replaceable (including React itself). So you can replace them one by one until you get exactly what you need.

Scaling

If your startup is global (and I guess every tech startup is supposed to be global), then sooner or later, you will start scaling. One thing that will happen, for example, is you will run out of downtime windows. In fact, it happens quite fast, you don't even need to have too many customers for that. For example, have some in Australia, some in Western Europe, and some in Russia - that's all it takes, you have 24 hours all busy with active users. If you're more B2B or a work time solution, this still leaves weekends, but deployment window of 2 days per week will never be good enough for a startup which undergoes a scaling stage. So it becomes really important to be able to deploy things without any downtimes, without stopping your services, ever.

So now we start talking about microservices and microfrontends. Your team will also start growing as you scale out, and become teams, so you'll need these anyways. "Independently deployable", "backwards compatible" and "rollbackable" become really big topics in your company, and if you're stuck with a monolith framework, well, you're in trouble.

I mean, some people say that monolith isn't that bad, and you can have a small team and still make a good money, and I do agree with that - in fact, I am a big fan of small teams and small companies. However, there are some important considerations here:

  1. In the scaling stage, adding more teams is the fastest way you can grow. Hypergrowth startups will usually choose this strategy and sacrifice efficiency and simplicity of a small team to ability to grow faster.
  2. I find it very empowering to be able to independently deploy parts of the application without affecting other parts, even when working alone. When you're running your service 24/7, it's a big deal!
  3. There are indeed examples of big companies who are still mostly monolith, but in reality, they are actively migrating to microservices (see for example: Github, Shopify), so there must be good reasons for that... 😉
  4. There are actually different ways you can build microservices, keep them bigger or smaller, keep them in same language or allow multiple languages, etc. - and a lot will depend on these decisions. But that is a topic of a separate article...

Anyways, the point is, in this stage, you're quite likely to be splitting things (not only code btw, so it is really great to use "infrastructure-as-code" - because then splitting non-code becomes splitting code!). Frameworks that tend to organize stuff by "category" (like Controllers, Services, Helpers, etc.) won't fare too well in this stage. Look out for frameworks that allow organizing code by business capabilities / features (they're surpisingly rare, btw). Also, with regard to CI/CD system, quite important to have an ability to target it to a particular folder/repo, so keep that in mind as well.

Choosing the perfect framework

So now we come to this really important question. "Tell me the name at last!" - say you.

But first, let's summarize, what are the main characteristics of a perfect framework for a startup:

  1. It is easily bootstrappable, has good defaults, can do most things for you, both on backend and frontend, and also with regards to developer experience (hot reloading, etc.) and deployment.
  2. It has full intellisense coverage
  3. It is very modular, and every module is replaceable, including the core of the framework, build system, etc.
  4. It allows to organize code around business capabilities rather than by category, and allows splitting this code into independently deployable chunks without too much effort.

Ok, to be honets, I don't know any such framework. There might be some candidates, but I don't have experience with them, so I will not recommend them here.

Also, frameworks quickly become outdated, so any such advice would probably spoil by the time you read this article 😁

But what I know I will do, if I ever start a startup again, is I will try to choose a framework that is really easy to jump off! 😅

For example, although JSX was invented as a part of React, it quickly became a standard, it is currently supported not only by every single bundler solution out there and natively by Typescript, but also it is used by multitude of different other frameworks, and also you can write your own createElement function for that matter, i.e. create your own implementation, fully tailored for your specific needs. Which means that if you're choosing a framework that supports JSX, not only you benefit from best possible intellisense, you can also replace it any time without having to rewrite your templates!

Another example, if you're writing your backend code, don't use any framework-specific features that are hard to replicate, inside of your endpoints. Make sure that you can always replace this framework with e.g. express or even pure http.createServer.

Let's say, if you're writing an Azure Function, don't use context inside your main code, instead, make a wrapper that does something like this:

const index: AzureFunction = async function run(context: Context, req: HttpRequest) {
    console.log = context.log.info;
    console.warn = context.log.warn;
    console.error = context.log.error;
    console.debug = context.log.verbose;
    return myEndpoint(req.body as IMyEndpointParams);
}

export { index };

So you're not using Azure Function specific functionality from your main code, which gives you ability to easily migrate from Azure Functions to e.g. Azure VM or AWS lambda or whatever else, by simply replacing the wrapper, without a need to rewrite the endpoint logic.

Word of caution: when abstracting away from frameworks, it is easy to overdo. Don't. Just a small wrapper, 10-20 lines of code - fine. A lot of code - either you're doing something wrong, or it's time to start looking for another framework.

Conclusion

Startups are hard. A lot of work, a lot of gray hair, and potentially, very big consequences from wrong decisions in the beginning.

So, let's choose frameworks wisely, and first and foremost, let's make sure it would be easy to replace them, if such need ever arises!

Comments

comments powered by Disqus