The software development world has changed dramatically over the last few years. Following the SaaS revolution, almost no software products are sold today outside a cloud and subscription model. The complete adoption of the cloud model has laid the ground for a major rise in new SaaS architecture concepts.
While Monolith applications were the standard 15 years ago, microservices reign on the backend side today. More and more applications are being developed as micro frontends as well.This post will follow the evolution of SaaS architecture: from monolithic applications through the microservices era, to today’s micro frontends. Our demo application for this use case will be a SaaS product called LoudAPI. The product provides API and services monitoring through it’s analytics dashboard, APIs and services monitoring dashboards, audit trails and configuration.
The Monolithic era
The evolution of the SaaS architecture started with the Monolithic approach, which is straightforward and the easiest one to build. You take ALL of your APIs, Databases, Services and UI, and push them ALL into one executable process. Our application will be bundled together, where the presentation layer will interact with the controllers to make API calls which interact with the database via a single data layer.
This approach (below) is also known as the MVC (Model — View — Controller) approach, as the data access layer takes care of the Model, the Controller takes care of the logic, and the View takes care of the presentation.
The MVC approach will work most of the time when you want to build a really lean MVP through a quick hackathon. It’s more suitable for IO-driven applications than for event-driven applications.
The cons of the monolithic approach are clear (and it’s not as though I’m telling you anything new here ????):
- Continuous deployment (CD) difficulties — When working on large applications using CD, the application is subject to frequent changes (due to the high number of potential changes on a huge code base), which means more frequent deployments.
- Continuous integration (CI) difficulties — When working on a large application via CI, the number of tests running for each code change and each commit / pull request makes the process of pushing a single line of code extremely long, with tests often running for hours.
- Technological commitment — When building a monolithic application, the commitment for backend AND frontend technology makes it hard to add additional technologies to the product (if my backend is implemented using Java, adding golang for a specific use case is almost impossible).
- Scaling — Scaling a monolithic application means that you scale up ALL services and contents, which takes a lot of time to start and to execute.
Getting yourself familiar with the code — Monolithic applications normally mean huge code bases. When working as a new developer on a huge code base, the onboarding process gets really complicated.
The microservices era
So the next phase (after the quick PoC) in the evolution of the SaaS platform architecture is to build the SaaS application using microservices.
How should we split it?
- By domain — Mapping the autonomous domains and creating services per domain. When creating the microservices, we need to decide on the DB technology and the technology stack (framework, language etc.)
- By scale — For application paths that are likely to receive most of the traffic and become bottlenecks, we should consider separating to individual services / workers so we can scale them individually and select the right “tool” (framework, language etc.) in order to meet their specific requirements.
- By use case — If you have a specific use case which requires a special technology, splitting it to different services is recommended (for example, splitting your IO-driven Node.JS with your Golang networking modules).
Splitting our LoudAPI monolithic application to microservices looks like this:
Splitting the SaaS application architecture to microservices gives us a lot of benefits:
- Easier to maintain — Microservices are generally built in order to solve a specific problem and their code base is relatively small, making them easier to maintain and to onboard.
- Faster CI/CD — Huge applications take time to test, build, migrate, and deploy. When the services are small, the CI/CD process is much leaner and a lot quicker.
- Easier to test — As mentioned before, each microservice is meant to solve a specific problem, making the testing of that microservice much easier.
- Easier to scale — The microservices architecture approach makes it much easier to control the scaling granularity of the service. If one of the flows needs more computing and memory power there’s no need to scale the entire application, just the relevant microservice.
The micro frontends era
So we’ve scaled our application backend and started gathering increasingly more customers (the growth stage). Now we’re faced with the challenge of having our frontend as the application bottleneck.
Every change which requires a UI tweak depends on UI developers’ resources, which makes the development process suboptimal (to say the least).
In addition, how can we build a domain-driven team if all the frontend development is still being handled by the singleton UI team?
How do we deal with the monolithic frontend? Well, how about micro frontends?
The idea of developing micro frontends is to build a web application as a set of independent features and components built by independent teams. When all components and features are hosted on one UI shell, the experience of the end user is seamless.
Using this approach, we can easily reach independent product and feature teams consisting of UI, backend and Devops engineers which can take care of their own product / feature lines without dependencies on other teams. Cool, isn’t it?
Using the micro frontends architecture as part of our SaaS architecture we will result the following architecture:
Each of the domain-driven components is being developed on the same team with the backend developers, meaning closing the loop much quicker and eliminating unwanted dependencies.
The communication between the components and services goes through the API gateway, and the application infrastructure shares the authentication token and gateway URL with the domain-driven components so they can access the relevant services securely.
The micro frontends approach is available through two deployment models (speaking from the frontend side):
The Multi NPM Module approach:
According to this approach, each domain-driven team is responsible to publish an NPM library and to trigger a build and deployment operation for the entire dashboard. Upon publishing the new dashboard build, system users can see the new UI.
The pros of the dynamic module resolving approach:
- Individual teams — Each team owns its domain and can independently develop its own UI.
- Version control — The product / release manager can determine when to build and deploy the main UI version in order to have each version tested, documented and notified (release lifecycle).
However, the cons of the dynamic module resolving approach:
- Main build bottlenecks — Since each minor change on the dependent NPM library causes the entire build for the triggered and built, it can cause bottlenecks in the CI/CD process
The Dynamic Module Resolving approach:
When using the dynamic module resolving approach, the main dashboard UI is built using placeholders for each of the domains without actually including them in the build.
I know it sounds weird, but bear with me…
This approach’s flow starts the same, but then takes a detour from building the new version of the dashboard. This is what it looks like:
- Domain-driven team builds a new version of UI for the domain-driven component
- The new version of the component is pushed to the NPM
- The new component is pushed to CDN as well
- The new deployment is updated on the company’s deployment registry via REST API call
- The next time a user logs in to the dashboard, the dashboard checks for the latest published version of the component (via REST API call), downloads the new version and extracts it to the placeholder
The idea of downloading dynamic bundles and extracting them to their placeholders is applicable using the awesome webpack capabilities available on modern UI frameworks today.
The pros of that approach:
- The main UI dashboard repository can stay without any change where each of the teams takes care of FULL END TO END deployment including the CDN registry and the deployments registry.
- Rolling back a version can happen with a simple REST API call (just update the deployments registry with the previous version, and you’re good to go).
However, the cons of that approach:
- Having to build the infrastructure for loading bundles dynamically based on deployments registry.
- Having to build a dedicated release lifecycle based on dynamic bundles and deployments.
What’s next with SaaS architecture?
The Jamstack architecture and where it fits in.
Over the last couple of years we’re witnessing a rise in the popularity of the Jamstack concept.
In most cases, referring to Jamstack is done for low-code / no-code web applications, but we can find a lot of conceptual similarities between the micro frontends approach and the Jamstack widgets approach.
Jamstack apps essentially enable operational excellence, reliability and security. This is because they minimize the moving pieces required to deliver the application, while giving it limitless scaling capabilities. There is also less exposure of attack surfaces for bad actors.
So: Could it be that the world of SaaS architecture and development is approaching an era where the company domain-driven teams will build the core functionality of the SaaS applications, while the rest of the business agnostic components will be micro frontends on your SaaS application?
Looking at SaaS architecture and development trends, and given the fact that the technologies and concepts are already there, I think that the answer is clear.
Sounds interesting? Watch out for the next installment: Using Jamstack to develop a secured SaaS widget end to end.