Building a Serverless Web App: Why You Should Consider the Monolith

Ashan Fernando
Bits and Pieces
Published in
6 min readApr 16, 2020

--

With the technology advancements in the cloud, it is possible to build full-stack web apps using Cloud native technologies, which are often called Serverless. Though it is a natural choice to go ahead with Serverless Microservices, for many instances, it becomes overkill. The fastest way to develop the application is by using the Serverless Monolith approach. If we take AWS, for example, you can build a Serverless Monolith by using a single AWS Lambda function for the backend.

Besides, it is likely that you write more code in the frontend where you actually need to look at breaking it down into smaller pieces.

Serverless web app with a single Lambda function

Since Microservices is the buzz word these days, it is essential to remove the misconception that Monoliths are bad. Monoliths might be the best choice unless your application and development team is large enough to experience Monolith’s limitations.

What about the Frontend?

Deploying the Frontend in AWS is quite straight forward. You can use AWS CloudFront to route requests and Amazon S3 to host the frontend artifacts. For more information about the technology stack, refer to my article on Full Stack Serverless Web Apps with AWS.

As mentioned earlier, it's better to build your frontend code as independent components right from the get-go. Your code will eventually get compiled into a single monolith but that does not take away all the benefits of a highly modular codebase.

Learn more:

Starting Serverless Monolith with Lambda

When we consider DevOps frameworks like Serverless Framework, it expects to point each route to a separate Lambda function. That means, let’s say you have the following routes.

  1. /users/1 (GET)
  2. /users (POST)
  3. /users (DELETE)

It requires implementing each operation in a separate Lambda function, which in my opinion, is an overkill in terms of separation for most of the practical use cases with Web Apps.

You can find various Serverless Coding Patterns and their benefits in the Serverless Architecture Code Patterns article.

However, when moving to the monolith approach, AWS API Gateway forwards all the traffic to the same Lambda function. Here the API Gateway only acts as the request trigger for the Lambda function. Then for each routing path and HTTP method, we have to write conditional handling to execute the appropriate logic based on event path and event method (GET, POST or DELETE) inside the Lambda function code itself.

Writing if-else statements or switch statements isn’t as clean as you think. Even if we separate this logic to a handler function or a separate file, still the challenges remain the same, where you have to make sure no route and method combination overlaps with each other. Besides, you need to edit this file when you introduce or modify a new route that could potentially add conflicts.

Moving to ExpressJS Like Route Handling

But isn’t the above problem already solved in ExpressJS like frameworks, where you can register a listener inside code, instead of centralizing to a single place?

api.get('/users/:id', (req,res) => {// do something})api.post('/users', (req,res) => {// do something})api.delete('/users', (req,res) => {// do something})

But how can we do this with AWS Lambda? Do we need to use ExpressJS itself? The good news is, you don’t need to use ExpressJS inside Lambda. You can use a handy library called Lambda-API that will handle the routing problem. The above code example is from the Lambda-API NPM library.

Following MVC Pattern to Structure Code

When following the Serverless Monolith pattern, its essential to understand that your Lambda function needs to structure its code in a maintainable way. As easy as it gets, we can follow the well-proven MVC pattern, separating the request handling, business logic, and response messages.

You can follow the below structure to organize your code effectively. I will use an example for NodeJS considering Lambda-API compatibility. I would recommend considering using Typescript instead of JavaScript, but it is entirely up to you to decide.

  • Create a startup.js, which is the entry point of your function. Inside the file, you can use an Inversion of Control (IOC) container like InversifyJS to register all your dependencies, including Lambda-API instance itself. Note that you need to manage the lifecycle of the instances per request lifetime to avoid any undesired behaviors.
  • Organize your routes in groups as controllers where you register the Lambda-API listeners for each route and HTTP method.
  • Separately structure your business logic as models or services inside a self-contained folder structure where we can pass dependencies from outside using the IOC containers. This approach allows you to unit test them outside the Lambda runtime.

How easy is it to move to Microservices later on?

Well, if your application grew over time and decided to move towards Microservices, you can break your Backend API into smaller chunks. If you maintain a folder structure as mentioned above, you can choose to create a new backend and move some of the routes and business logic over there. For shared dependencies across Microservices, you can consider using Lambda Layers. Still, you need to find ways to handle the data separation depending on your bounded contexts.

Summary

You might wonder, is it worth to try the monolith approach in the beginning and move to Microservices someday? Is it worth the efforts?

Just imagine if you had to think of shared dependencies, data separation, transactions across Microservices at the beginning of a project in contrast to following the Monolith approach. It could have slowed down your development with a significant margin for a small team. You might speed up your development without dealing with distributed systems even for a couple of years, building a competitive advantage, where you have sufficient knowledge, capacity, time, and money to consider Microservices.

Besides, the monolith approach doesn’t limit you from building multiple APIs if you have a clear separation between them. For instance, if you plan to write an API for B2B integration, you can develop it separately from the backend of your web app as a separate Monolith. Also, there aren’t any hard and fast rules to only consider Monolith. For instance, if you have a specialized operation that requires a significant amount of resources, you can separate it from the Monolith web app backend and put it into a separate Lambda for apparent reasons.

Overall, the Serverless Monolith pattern provides a straight forward approach that is faster to develop. If you are new to Serverless and Microservices, I would say this is the way to go. Otherwise, do consider the overheads, before trying to break things into smaller pieces and do it smartly.

At last, smaller pieces of code with separate runtimes don’t mean that it’s always easy to maintain. Remember, whenever you consider coupling as a problem, there is the cohesion that needs to be balanced.

Learn More

--

--