Part 5: Micro-services & Umbrella apps
Just like Erlang poses the question ‘cattle vs pets’ in terms of deployments, Elixir Umbrella applications and Erlang distributed applications make us rethink micro-services, mono-repos and why exactly we want micro-services in the first place considering many of the inconveniences and complexities they introduce.
Why micro-services
As I see it, there are two main advantages of micro-services over a more traditional service-oriented architecture.
- Development - Although not a technical reason for micro-services, development can be significantly improved with micro-services. This includes considering and enforcing a separation of concerns, keeping codebases from growing into monolithic monstrosities, and simplifying management and access to codebases within an organization (using separate repos, directories etc). Micro-services are not a requirement to improve any of these areas, they only make it less difficult to violate the rules. In fact, I’d consider all of these superficial reasons, even if valid and useful, to using a microservice architecture.
- Individual scalability - The primary technical reason for micro-services is being able to scale logical or functional parts of your system independently of others. This gives more fine-grained control and isolation for independent parts of your system, which is a real advantage rather than superficial. For example, your API may have a particular resource with different SLAs, technical requirements etc that you want to scale, tune and optimize accordingly.
Analytics is a great example of what you'd want to scale independently. OLAP data, rather than OLTP data, is typically slower to process and is also usually a distinct feature of an application (visualization, for example). By splitting this out to a separate micro-service you can configure alerts to go off at higher timeout thresholds, scale up or down the APIs according to how common or uncommon these requests are compared to the rest of your API calls, and optimize CPU, memory, concurrency etc. based on the unique characteristics of the analytics data.
Where exactly the line is between micro and standard services are is unclear, but regardless these are the main motivations behind having lots of smaller services rather than a few large services; one indirect non-technical advantage and one technical.
With these 2 goals for micro-services, and one of them fairly flexible in terms of solutions, I plan to make an argument for the ‘Elixir’ approach of meeting these requirements.
Mono-repos
Many large companies, including Google, use monorepos to store all of their code. That is, instead of new repositories for each individual project or application they use a single, massive repo. For smaller companies this may not pose any major issues, though at Google scale there are some challenges in maintaining a repository of such massive scale.
The more realistic challenges a smaller company will face with a monorepo approach is how to handle deployments and releases of sub-projects in the same repo, which is a little trickier than a single-job-per-repo approach. Perhaps even more challenging is how to properly structure the repo to handle so many different and diverse projects.
Elixir Umbrellas projects
Elixir Umbrella projects are more like a mini-mono-repo than a true mono-repo. They can be used to store all of your Elixir applications (and even some JS projects) inside a single Elixir Umbrella application. Combined with the release/deployment capabilities of the previous section, Elixir Umbrellas can provide highly customizable and configurable releases and deployments from a single Elixir Umbrella project.
Scalability
With the flexibility of individually versioning and releasing each sub-application of an Umbrella, any of which can be composites of multiple applications, you can easily define your micro-services as individual applications or groups of applications. Each of those individual releases can be deployed independently, if necessary.
This addresses the more valid justification for micro-services; individual scalability. If you have a problematic application that just eats resources at a totally different rate than other applications in your system, you can use Distillery or Elixir Releases to configure a separate release for just that app and any apps it depends on. That release can then be deployed and scaled separately from the rest of the Umbrella apps.
Realistically you probably won't have a strong need for this unless you have very specific use cases or a complex system, and can usually just deploy the umbrella as a single application, but knowing you have this level of sophistication in building micro-services simply through configuration should really stick out compared to existing solutions available today.
Development
So now you have dozens of applications in your Elixir Umbrella. You have your website, your product GUI, several different API applications, and some data pipelines, jobs and specialized services all inside your Umbrella, and have 2-3 releases configured to group and scale these apps logically or based on business needs. The Elixir Umbrella seemed to get the job done well for the 'individual scalability' feature of micro-services, but what about code organization?
More often than not I think micro-services are defined by the teams that will be working on them rather than how they fit in the problem domain, and let's face it, nothing beats a brand-new empty repo for that new project.
The reality is microservices are typically used more for humans than machines. It makes it easy for us to prevent the dreaded monolith and keep our codebases small and simple, while also permitting us to scale the service independently in anticipation of high work loads.
While sub-apps in an Umbrella can certainly help isolate code for a specific feature/application, they are still part of the same project and can easily rely on other apps in the Umbrella, which means now you just have a jumbled mess across multiple directories! Luckily Elixir does have a solution to this problem as well, called Contexts.
Phoenix Contexts are an effective way of addressing this problem when using the highly-recommended Phoenix Framework, and are used specifically to better isolate and decouple parts of your application that may otherwise become intertwined and a jumbled mess. When used in conjunction with separate Umbrella applications they can really help give your codebase the tools it needs to establish proper boundaries now and in the future.
Discipline
In conclusion, Elixir Umbrella projects and releases solves the technical issues in an Elixir-based micro-service architecture, and does so in a manner that is as sophisticated and elegant as any solution out there.
In regards to the human and organizational benefits of micro-services, Umbrellas do support multiple applications for better code isolation and Phoenix Contexts can help better separate code that lives in the same Umbrella. However, at the end of the day, discipline is still needed on behalf of developers to ensure the proper separation of concerns is part of the system design and adhered to throughout the development process, and the only way to know that'll be done for sure is independent repositories.