While working at Kamet, I’ve worked with many early-stage ventures. In this article, I will share my thoughts on different paths that startups can take regarding technical architectures.
I believe that a startup should prefer as much as possible a well-decoupled monolith than a micro-services architecture for as long as possible. Given the hype of the “functions” (i.e., serverless), it’s worth clarifying that they count as “micro-services” as they have almost the same characteristics (minus a bit of ops-headache).
Why are micro (functions or service) architectures so popular?
We can’t argue that they are super en vogue, probably because they are used successfully by a few global companies like Netflix or Spotify. They are role models of engineering teams but have entirely different problems than an early startup.
Premature optimization of the first cause of failure in startups. Building systems far too complicated for what we need or trying to solve problems we don’t have yet but rather imitating other big corporations. George Hosu’s article “Stop future-proofing software” is an excellent read on that topic.
I’m biased by experience as I’ve already tried and failed (we went back to the monolith for many reasons) deploying such architecture too early. I’m not the only one thinking this way either. ThoughtWorks calls it the “microservice envy”.
Multiple facets comparison
In his article about “monolith or single-purpose functions”, Yan Cui uses the following scale to compare the pros & cons:
How can your clients base and your engineering team scale?
How is it to understand and fix a given issue?
How easy is it to find a specific piece of code.
Using the same scale, I will go through each of them and highlight why these monoliths have a higher value for startups.
Scaling your users
Surely this has nothing to do with your architecture for an early startup. WhatsApp is a good example: they successfully used a single machine for a very long time.
Your software needs to be as stateless as possible to be able to scale horizontally. It’s easier to horizontally scale a monolith: imagine having to set up and maintain this authentication layer on every of your micro-service? Nothing to worry about here.
Scaling your engineering team
Scaling your team This is the central promise of micro-services (and “functions as a service”): being able to split your team(s) into multiple independent ones.
You might have a few engineering teams (or squads if you want to be inspired by “Spotify model”).
- Realistically, they are not independent.
The first years, and especially if you are still in the product-market fit phase, your teams are not independent. Even if you split your teams into radically different features set, one is going to consume another team’s data and the other way around. Dependencies are hard to manage but are necessary.
- Decoupling is the only key
- Decouple over a method call than network hops
You don’t need to couple via HTTP APIs (or asynchronous function calls following an SQS message). If you do go this route, you will have to assume the fact that you need to handle errors asynchronously and that every of these “calls” might fail because of issues outside of your control (i.e., network or infrastructure failures). Exposes these methods via an HTTP API for your partners if you need to, but don’t necessarily need to impose yourself the constraints associated with it.
One argument I’ve heard multiple times was that with multiple repositories it would be “simpler”.
- A lot of counterexamples
Google and a lot of others are using monolith repositories with a lot of success (different scale though, I have to admit ;-)). One of the key counter-argument seems to be that it’s hard to run continuous integration scripts only for what has changed in a pull-request: let me save you the Googleing pain:
git diff master — . | wc -lwill return
0if the current directory did not change compared to the
- Reduced discoverability
As discussed in the section “Discoverability”, having multiple repositories forces the engineering team to know which repository they should go to search for some code.
- More “ops”-related work
You need to configure and maintain the continuous integration and continuous deployment pipelines for each of these repositories. Did you ever have to migrate from CircleCI to Travis for example? Imagine doing this on 40 repositories ;-)
Yan’s second measure is the “debuggability”. How easy is it for a developer to find out why something is going wrong?
No local development environment
By going full blown micro-functions or even a few services, you quickly lose the ability to have a local development environment: you can’t run your software on your machine anymore.
- Why? Usually using cloud functions means you rely on your cloud provider for notifications (or queues) to trigger your functions. I can be as far as relying on them for the HTTP layer (i.e., AWS API Gateway). In this case, having these (as small as) 5 lambda running and calling themselves locally is hard.
- Isn’t it a good thing for tests? Surely the only way developers are going to know if their code is “working” is by writing tests then, therefore it should be good for the project.
- Associated risk: silos. If a feature team can only run their own “feature” locally (if they can), can’t this push them to consider only their local functional scope without acknowledging that the expected outcome (for the whole company) is met?
- Associated risk: integration costs. Because it might be hard to test the full journey of a given feature
I think you get the picture already: many small functions or micro-services is inherently distributed computing. And it comes at a high cost.
- Aggregated view of “what happens?”. Imagine an HTTP request that just failed. It went through 4 different functions before the actual error occurred. Even with great tools like Amazon X-Ray, having a clear picture of what was the exact situation that triggered the issue is often hard. You probably need to set up something like OpenTracing in your infrastructure so that you can executions or requests across multiple spans in your architecture. It’s great, you will be able to understand which execution triggered which one at the end but it’s hard to set up and maintain.
- Sentry or similar? You need to go through a lot of these different projects to be able to correlate the final issue your customer has seen, this adds a lot of noise.
The ability for a developer to figure out where is the logic associated with a feature is key for at least two reasons. First, you want to be able to onboard new developers as fast as possible. Second, you want all of your developers to be able to easily add a feature or fix a bug they’ve seen in a part of the codebase they don’t touch every day.
- “In which codebase should I have a look to?”
Having multiple code repositories forces this question. Looking at every code repository on your organization’s GitHub is pick-and-try repositories is not the most efficient way to grasp where a feature is located. You can have a central documentation that describes where are the main silos.
- Different codebases remove the visibility on other teams’ code style or structures
Do you use “modules” within your application? Do you use styled components? Atomic design? Are the tests a generic `tests` folder next to the `src` folder or are they within each independent feature? All these questions are super important for a development team and should be asked. On an organizational level and for the sake of fluidity between teams, the best is to have common standards. Within the same codebase, it’s relatively easy to spot discrepancies and get inspiration from other teams which leads to common standards across the codebase. Within a multi-repository configuration, where a developer is likely to only look at one or a few repositories, discrepancies might appear much quicker.
- A feature within the source code
Within a code base… how do I find this particular piece of code? This question needs to be answered regardless of the monolith/micro-service question. In my experience, a modularised (feature based) approach to the code structure helps to directly focus on the few important folders of a given feature. Typically, you can navigate to
Okay, monolith… but what about all the advantages of functions?
Let’s review a few points that, in my point of view, makes cloud function not ideal for startups.
- “No ops”
That’s a fantastic promise; I have to admit. Deploy your monolith on a PaaS like Heroku or even as a single monolithic “cloud function”. You’re done.
- Separate deployments
Well, first the good is that you only have a single deployment pipeline to configure, your. What’s the issue you’re trying to tackle by having different deployment pipelines?
- Ordered service dependencies
That doesn’t exist in a monolith; you don’t have to worry about this anymore.
- Is a feature not ready?
At some point, with or without micro-services, you’ll need to toggle features depending on your clients. Use feature flags within your monolith, don’t push this responsibility on the infrastructure.
I believe that the only constraint to growing the ability to properly split the effort into separate teams working on decoupled features. Dividing things into many small components deployed independently and talking to each other over network adapters has a lot of complexity attached to it so be sure to feel the pain of the monolith before splitting.
As already stated in the introduction, this article is a summary of my thoughts regarding the monolith repository vs. “micro-services” or “cloud functions”. I’d, therefore, love to hear your thoughts.