Engineering in startups is different. With lower product maturity, fewer resources, and less certainty about the future, startups have a whole bunch of constraints that must be taken into account when designing the product’s architecture and engineering procedures. Simply sticking to the latest engineering trends – will not necessarily be ideal for startups, and might put them in real danger of running out of business.
While it’s pretty obvious that startups must be fast in the way they develop their products and respond to their customers’ needs, many startups often find themselves collecting endless technical debt (“meh, we’ll get to it in the future, say, next year”), resulting in a massive impact on the agility of their team and their ability to remain fast in the short term.
So how can startups be fast, and stay fast? In this blog post, we’ll dig into some of the dirty details of implementing some of the industry’s most popular trends – such as Microservices, Continuous Deployment & TDD (Test Driven Development), and provide some tips and best practices for implementing a scalable product in a startup in 2021.
Monoliths – aren’t they extinct?
From a quick peek into Google Trends, we can see that unsurprisingly, “microservices” as a keyword has exploded in the last few years (with a small decline at the early days of COVID-19, when people were probably more concerned about other things, such as toilet paper and masks).
However, in the past few years, we still keep on hearing about engineering teams that are “breaking down their monolith”, while migrating their production workloads to run on a microservice architecture. But why do we still hear about so many companies that choose to start off their product using a monolith architecture, when microservices exist for several years now? Alternatively, isn’t it possible to start a fresh new product using a microservice architecture from day one?
Before we start answering some of these questions, first – let’s align on some of the basic concepts of a healthy microservice architecture.
Microservices Architecture vs. Monolithic Architecture
A monolithic application is built as a single, indivisible piece of software, and its most significant advantage is keeping it simple. It is easy to set up, simple to monitor, debug, etc.
Its big downside, however, is the fact that it doesn’t scale – not only from performance aspects, but also in terms of deployment, maintenance, and collaboration across multiple development teams.
The trendy microservices, on the other hand, are very scalable. Their main benefits are:
- Microservices are independently deployable
- Fault isolation
- Clear ownership among multiple teams
- Can use different programming languages
- You can say you’re doing Microservices (Trust us, it’s fun)
Trend #1: The Microservice Reality
So what happens when start-ups want to join the microservices trend? Well, they discover pretty quickly that alongside the benefits, this trend is also causing them problems. Here are some of them:
Problem #1: Code Duplication
One of the things that most characterize startups is that on a daily basis, they generate about ten times more code than large organizations do, or as we like to call it: 10 times more code, 10 times more pain.
If you are setting up a new microservice, no matter what programming language you use, you will find that you have to write a lot of boilerplate code.
One example would be enterprise utilities. Most organizations eventually end up creating various internal methods and utilities that perform logic that cannot be replaced with any of the existing open-source packages out there. Alternatively, there is no high-quality, popular, and highly-maintained third-party package that answers the business needs – but only niche packages that are not as reliable., Consequently, you’ll find out that you need to start duplicating all sorts of bits of code between microservices.
The same goes for different kinds of configurations as well, such as Build, test, Lint, or even Git configurations. You may create a configuration and end up duplicating it, or have each team create configurations for itself, resulting in a new problem of consistency across the entire R&D organization.
The same is true for data. One of the most important things is that each microservice is responsible for its own data. But no matter how much effort you put into the model of your microservice architecture, there will also be some level of duplication between your microservice data schemas. This inevitable overlap in your database will eventually lead us to data duplication issues.
Problem #2: DevOps Overhead
DevOps overhead is a significant and painful problem, which came about because of the transition to microservices. After all, when choosing microservices over a monolith, we are exchanging the complexity of the monolith with the complexity of architecture and networking of the microservices. Here are a couple of examples:
- Service interfaces – one of the issues we have to face is the interface. What happens when my microservice wants to communicate with the outside world? For example, another microservice, storage, or third-party application?
- Databases & Data migrations – you may have several databases. You may have one database with many more tables, creating one of the most painful things in a highly dynamic start-up reality – data migrations.
- Deployment & CI – now that you have multiple services, you need to define the deployment process for each and every one of them, while taking into account different dependencies and backward-compatibility considerations.
Problem #3 – Observability is Painful
Last but not least, we have monitoring & logging. Monitoring is a very significant problem that many startups in the industry are trying to address. The decentralization of working with microservices made it very difficult to see the big picture. When there are many teams with many microservices, there usually isn’t one person who sees the entire picture.
This problem gets even more excruciating when you need to urgently debug an elusive bug. In a start-up reality, sometimes one customer can make all the difference. If the largest bank in the world becomes your customer, that alone can lead the company to an exit. In this reality, giving a quick response for the customer is super critical for the start-up, and debugging can be the deciding factor.
The Flexible Alternative: Miniservices
In light of all the problems we mentioned above, we started looking for a solution that would give us a little more flexibility, and we found it in miniservices – a flexible microservices architecture, or as we like to call it: microservices’ cousins. Some of the basic concepts that were considered an anti-pattern in a Microservice architecture, might actually be common in a Miniservice architecture.
Here are some key miniservices principles:
- Give up on architectural perfection in favor of business value
- Easily extensible to a full-blown microservices architecture
- Miniservices may share data schemas and databases
- Communication methods may vary
- Typically, a miniservice architecture will be consisted of less services
- Typically, a miniservice architecture will include less data stores
- Code sharing between miniservices is more common (but still not a best practice)
The main idea behind this concept is to note that most companies do not really operate on production workloads in a magnitude similar to Netflix, Amazon or Google. Prioritizing business value over the architectural perfection of microservices is probably the right way to go for most of the smaller companies in the industry, as the upsides of this approach are much greater than the downsides.
Trend #2: Multi-repo Vs. Monorepo
Microservices are not the only big engineering trend that is happening right now. Another big trend that naturally comes together with microservices, is using a multi-repo version control approach.
The multi-repo strategy enables the microservice team to maintain a separate and isolated repository for each responsibility area. As a result, one group may own a codebase end to end, developing and deploying features autonomously.
Multi-repo seems like a great idea, until you realize that code duplication and configuration duplication are still not solved. Apart from the code duplication that we already discussed, there is a whole new area of repository configurations – access, permissions, branch protection, and so on. Such duplications are expected with a multi-repo strategy because multi-repo encourages a segmented culture. Each team does its own thing, making it challenging to prevent groups from solving the same problem repeatedly.
In theory, a better alternative could be the mono-repo approach. In a mono-repo approach, all services and codebase are kept in a single repository. But in practice, mono-repo is fantastic if you’re Google / Twitter / Facebook. Otherwise, it doesn’t scale very well. Basic operations such as cloning the repository or searching for some keyword in it, become slow and heavy. Also, it is important to keep in mind that many CI/CD/VCS and tools that integrate with your VCS work much better out-of-the-box with multi-repo.
Our approach to this dilemma: we use a mono-repo that could be easily separated to multi-repo as it scales. We also share code via internal libraries (Our case: NPM packages) and use symlinks for LINT, test configurations, etc. We also use a multi-repo code-sharing alternative like Git submodules, and this combination is working for us very well.
Trend #3: Continuous Deployment
Continuous Deployment (CD) is a software release procedure that utilizes automated testing to determine whether changes to a codebase are correct and stable enough to be sent to a production environment immediately and autonomously.
Continuous deployment has some great benefits. Deploying changes to production in minutes, the short feedback loop, and it goes very well with TDD (Test-Driven Development). More than once have I seen an enthusiastic software developer describing a perfect CI/CD setup: “you submit a Pull request, get your code reviewed and approved, the tests run and pass, and your code is deployed to production within minutes!”. The problem? This magical process comes with a significant price tag:
- Very high test coverage is essential
- E2E/Integration tests may require complex infrastructure
- CI gets slower with time
- Tests take time to write
- Tests take time to review
- Tests take time to maintain
Our flexible approach to this problem is to use the standard testing pyramid, but on a diet: we make calculated investments in testing, relying mainly on unit tests for critical paths, with a few end-to-end sanity tests for the entire application. In addition, we heavily rely on an internal staging environment and on heavy investments in monitoring and alerting infrastructures.
While Microservices are growing in popularity, it is important to keep in mind that they come with a price. Investing heavily in a perfect microservices architecture may cause early stage startups to move more slowly, while struggling with the pains of code duplication, devops overhead and growing debugging tasks. In a startup, agility and flexibility are key to success in almost every aspect – and they rely on having a smart engineering architecture for your product. Miniservices themselves do not provide a perfect engineering architecture, but can definitely help you achieve a hyper-agile engineering culture that can balance between business value and technical debt.
Want to help your startup development team work more efficiently? Check out Hysolate Free, a Windows 10 sandbox on steroids.