We survived 2 heavy pivots on Scrimmage. We started as a B2C analytic product, then we pivoted to a WEB3 play-to-earn game and then we became a B2B SaaS product. During the first pivot, our whole codebase was archived and it took us 5 months to be production ready with a product. During the second pivot though we reused 90% of a code base and it took us 1 month to be production ready. As a startup, you believe in what you are doing but pivots are inevitable. You wanna build a product that will be able to change its face as the market demands. Here is how we did it.
We build healthy abstractions
It took me 4 years of programming to learn how to build healthy abstractions. The idea is to be as less specific as possible while naming your variables and classes so that you can apply it under different circumstances.
For example, we were building a play-to-earn game with a dragon character who can level up and complete quests. Instead of calling variables “dragons”, we call them “NFTs”. This small shift in naming will make you think more abstractly. Instead of thinking “What dragons can do”, you think “What NFT can do”. Now you can easily reuse this class across all your future projects which will utilize NFTs.
Another common example is trying to separate architectural abstraction from app-specific logic. If you are building architecture as a code solution or just simple CI/CD, don’t call things by their names call it by its purpose. If you have 10 microservices you may wanna create a Docker file and CI/CD file for every one of them, instead of you can create an architectural abstraction called “deployable” which will have a default implementation of a Docker file and CI/CD and will require from microservice just a couple of input variables as name, port, and image. K8S doesn’t have to know about your app logic.
If you follow healthy abstractions you will be able to open a new way of writing code – building mechanisms that can solve problems instead of solving problems directly.
We build mechanisms that can solve problems instead of solving problems directly
The idea behind it comes from a desire to support all future business requirements without changing the code. If you noticed that there is a file that is getting changed very often, it is time to build a mechanism for supporting further changes without changing the code.
An example is how we organized our reward program. So the idea of the app was that users can make bets and we give them tokens for every bet they make. Instead of calling “bet” a “bet”, we called it “rewardable”. Instead of hardcoding how many tokens to reward for a bet, we implemented a mechanism that allows us to configure reward per “rewardable” from the DB. Now we can reward tokens for anything without changing a line of code.
Another example, we wanted to reward users more when they win the bet. Instead of writing code like “If rewardable is a bet and if this bet has status WON, multiply reward by 2”, we implemented a “modification” mechanic. Modifications allow us to modify rewards based on predefined filters. You specify filters in the database and this filter will automatically be applied to every reward. For example, we have a filter “When rewardable type is bet and field OUTCOME has value WIN, multiply reward by 2”. Tomorrow client comes and asks to reward people more when they lose. We can do it from a database by changing 0 lines of code.
This code is priceless and can be reused in millions of ways to support billions of use cases. Such code takes longer to write but you will spend much less time to support it.
If you build healthy abstractions and use this abstraction to create generalized mechanisms you could think of moving some of the mechanisms into separate microservices which know nothing about the rest of the system.
We build microservices that can be open-sourced
Imagine that your microservice will be used by thousands of other developers in completely unrelated projects to yours. This idea is making you reimagine the dependency which your microservices are having between each other. It is preventing our code from dependency hell and making it reusable through all future projects and pivots.
An example is the scheduling mechanism we have implemented. We started our app as a singleton with cron jobs that are sunning from code. The problem is that we can not scale this service horizontally so we had to separate scheduling logic elsewhere. Instead of setting up cron jobs using code, we have implemented and constantly added new values to them, we have implemented a K8S-Scheduler which is a helm chart that is getting deployed in our cluster. This helm chart scans all our microservices for cronjob definitions and executes them using API calls. Now we can describe cronjob while creating a new endpoint and a scheduler will start scheduling it without us touching a code.
Another example is the authentification of a user using JWT. Instead of making it a part of one of our microservices, we made it a separate microservices which authenticates users and returns a JWT token that can be used across all microservices. Now this microservice can be reused on any future pivots or project by executing single helm command to deploy it.
If you follow this principle after 2 years you will have a library of 10 microservices that can be used to build any kind of product related to your industry in a matter of days. Now you have a constructor of products that allows you to build products using just configurations.
We build product constructors instead of products
You may call it over-configuration but we call it architecture. Now when you realized that your product doesn’t have a product market fit you can quickly pivot by changing a couple of variables in your database and deploying some of your past microservices to a new cluster. However, managing all those microservices may be hard.
Terraform serves us well on this point. It is providing us with an architectural abstraction that allows us to look at the infrastructure as a code that can be easily modified, looped over, and configured. If you need some new functionality that you don’t have yet, you already is having a clear interface to create that peace and add it to the terraform which will combine it with all other microservices. It will automatically add auth to it, making it observable, deployable, and schedulable. It will also pass all required environment variables and host it under the public ingress.
We implemented a system that makes us able to develop software using configurations. It has to be the end goal of every IT product which wanna be able to sustain dozens of engineers under its roof and survive future pivots.
Not every software needs this approach. Sometimes you wanna build a quick prototype that will test the market. But it is important to differentiate a dirty prototype from an MVP that has a long life ahead, full of pivots and requirement changes.