Microservices and Serverless are two of the hottest and trending topics in the current IT industry. Designing Microservices applications has always been a challenge and with Serverless added, it becomes more difficult and needs a different thought process all together.
The purpose of this Blog Series is to understand the various Architecture Patterns available when working with Microservices using the Serverless ecosystem.
Part 1(This blog) — How to structure Microservices using AWS Serverless stack and using Synchronous Request-Response pattern with API Gateway and Lambda
Part 2 — Covers Designing of Asynchronous event submission using API Gateway, SQS and Lambda. And how to implement Saga Orchestration Pattern with Serverless Step Function service.
Part 3 — Covers approach of sending the status of asynchronous task submission, in our case New Food Order Placement, to the client.
Part 4 — Covers Placing New Food Delivery Order using Choreograph Saga Event Driven Pattern.
Part 5 — Using Serverless stack for designing Notification Service.
Let’s jump in — Team XYZ wants to build a food delivery platform (YAFDP — Yet Another Food Delivery Platform). Using Domain Driven Design approach — they have come up with few of the services (just for reference purpose) having a well-defined boundary context —
- Order Service
- Restaurant Service
- Delivery Service
- Loyalty Service
- Payment Service
- Notification Service
- Identity Service
Users will interact with the Platform using Mobile Application and Single Page Application for Web.
Team wants to start with the design of Order Service and Restaurant Service which will sit at the edge, handling requests from the end users. The basic architecture pattern for it could be —
Order service shall be owned by Team O and Restaurant Service shall be owned by Team R. But both Teams got one question — how do we pen down things? And they come up with below approach —
This approach looks like the way Microservice is implemented in a Non-Serverless world. It has —
- One Monolithic Lambda per service and each Lambda function has got if/else logic to execute the functionality depending on the routes.
- API Gateway implements a Proxy resource per service — something like /orders/proxy+ with ANY method for Order service and it forwards all the order related requests to Order Service Lambda.
- Single Code repository for hosting both the Lambda functions and other AWS resources like API Gateway, DynamoDB, Roles etc. needed for the services to be up and running in AWS environment.
In first glance, this looks OK —
- Clear separation of functionality — Single Lambda function per service.
- Number of Lambda functions to manage is less — one Lambda function per Service
- Deployment is fast as just one Lambda function to be deployed per service.
- Because the same Lambda function is serving all the features, probability of hitting a Warm lambda is high. So less cold start instances.
But it has got few issues —
- As more and more service level functionality is added in the single Lambda function, it gets bloated with more code and external libraries and can easily hit the upper limit of the package deployment size.
- If a fix is done in a specific feature (route) of the Lambda function, you don’t have the option to deploy only that feature. The entire Lambda function has to be deployed.
- Lambda Function also needs to handle Authentication and Authorization for the routes invoked.
- Tough to leverage API Gateway native features.
- For debugging, you have to go through the entire Lambda function code, understand the if/else ladder and hence it can lead to high turnaround time.
- While deploying the Lambda function, you need to perform Unit testing, Integration testing, etc. for all the features.
- The Blast Radius is high — if a vulnerability is found in a 3rd party library. The Lambda function will have a single execution Role attached with the required read/write permissions to access other AWS resources.
- Conflicts between Teams in terms of ownership of API Gateway resource.
Ok. So how do we solve the issues? With some internal discussion, teams came up with below revised approach —
This approach has below advantages compared to earlier one —
- One Lambda per feature of the Service with Single Responsibility design principle. Get My Orders has one Lambda function, create new order has another Lambda function. Hence function code and its dependent library are low in numbers.
- Low Blast Radius — each Lambda function has got access to the AWS resource based on the functionality it implements. For example — Get Orders Lambda function will have only Read access to DynamoDB. So, should there be any vulnerability found in the 3rd party library, the Blast Radius because of it should be low.
- API Gateway native features can be leveraged. For example — validation of the payload, transformation etc. can be performed.
- Quick to debug and turnaround time as developer would know which Lambda function has got the issue.
- Clear demarcation in terms of ownership of the service, its configuration and other AWS resources required for the Service to function
- Code Repository Per Service.
The above approach has got few issues as well —
- The number of Lambda functions can grow like anything. Hence manageability could get complex.
- With more and more Lambda functions created, one can easily hit the 200 Resources limit of CloudFormation.
- Chances of hitting Cold Start for Lambda functions is high as we have one lambda per feature now.
Given the pros of the revised approach, one can go ahead with it. For the cons mentioned above, there are ways to handle some of them if not all.
So, now that we understand and freeze on the structure of the Microservice, the next question that comes up is — how do we access these services via API Gateway from outside? Well, there are 2 options for it —
Option 1 — Use custom sub-domains with API Gateway for accessing the services
https://orders.yafdp.com -> For Orders service
https://restaurants.yafdp.com -> For Restaurants service
Option 2 — Use single custom domain with API Gateway and path-based routing for each service
https://yafdp.com/orders -> For Order service
https://yafdp.com/restaurants -> For Restaurants service
The Pattern we discussed in this blog is the simplest Synchronous Request-Response pattern —
End Users Request -> API Gateway -> Invokes Lambda Function -> Read/Write Data from DynamoDB and response flows back.
In the upcoming blogs, we shall look at the next set of Architectural patterns that can be leveraged to implement YAFDP.
Till then. Cheers!!!