Conf42 JavaScript 2021 - Online

Embracing Hexagonal Architecture with Serverless

Video size:

Abstract

The hexagonal architecture, or ports and adapters architecture, is an architectural pattern used for creating loosely coupled application components that can be easily connected to their software environment. This makes components exchangeable at any level and facilitates test automation.

AWS Lambda provided the perfect environment to work with this architecture, moreover using hexagonal architecture, allows great flexibility to change computational layer in case of porting across cloud services

Summary

  • Luca Mezzalira proposes modular approach for structuring your serverless project using hexagonal architecture. Evolutionary architecture support guided incremental changes across multiple dimensions. Examinations allow you to drive an application from a user point of view.
  • We can easily use a function in order to communicate from the external world to internal worlds. What it does is literally mapping the request from the adapter to a specific function inside my business logic. We can also use a cache aside pattern to offload all requests from our application to a third party system.
  • With exceptional architecture, I found some use cases that I believe are interesting to think about when we want to use this architecture. The first one is testing because we are modularizing everything. We can really create different tests potentially based on tests for adapters and tests for business logic.
  • Another approach is web application modernization. Using a modular approach, you can extract pieces from a lambda or from a microservice very quickly. Another approach could be hired strategies. Despite it, only a new concept is evolving through different and used through different architectures.
  • Thank you very much for your time. If you have any questions you can draw me a line that is on the bottom left of this slide. Feel free to to contact me and I hope that you have a great rest of the office.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi everyone, and welcome to this talk about evolutionary architectures with AWS, Lambda and the Katner architecture. I think everyone at least once blamed the way how we have written the code in the past on our project, because evolving a complex code and maybe code that has a lot of technical depth is a challenge. And today I would like to propose modular approach for structuring your serverless project using hexagonal architecture. My name is Luca Mezzalira. I'm a principal solution architect at AWS. I'm an international speaker and a rally author. So let's start with the definition of what evolutionary architecture means directly from the book building hexagonal architecture. So evolutionary architectures support guided incremental changes across multiple dimensions. We think about that when we create an architecture. When we select an architecture, we need to find one that will allow us to follow the business drift during the journey. When they happen. We cannot anymore select an architecture blindly just because it's the one we are more comfortable with. We need to really understand our context and then apply the right architecture for it. And I believe that everyone at least once has to deal with a free tier architecture. And usually what happens in free tier architecture after a while that you are dealing with it is that the presentation layer and the data layer starts to leak inside the application layer. So these three layers became, the lines between them became a bit blurry. And the challenge then in the long run, is maintaining some code that has a lot of technical data and some logic that should live inside different layers instead that are living in other ones. And therefore that will require, that will cause frustration for developers and we require time and effort in order to make it right. And it's not an easy task, definitely. Moreover, what happened once to me was we wanted to move some workloads to Lambda. We had the workload either on MCs Docker containers or in MSQ machine and then we wanted to move to lambda. But that wasn't straightforward because the code that we have written was really tightly coupled with the implementation infrastructure and everything. Therefore, decoupling that code and moved to multiple lambdas, it was a non trivial action. And finally, I believe that you have seen more than once in articles, blog around the web and maybe even in your code base lambdas that are written in this way where you have your handler that contains multiple functions inside and for instance, in this case you have submit candidate that is calling candidate info and he, and then returns a response to the client. And when you start to dig into those two functions, you discover that submit candidate p at the end is let's say a logic port storing some data inside dynamodb. And candidate info is a value object. So we are basically merging inside the same file, the entry point of our lambda, the infrastructure as well as the domain. So in this case the value object, that is not exactly the right way to structure our code, because then if you want to evolve that, we can introduce bugs in several areas. And moreover, it's quite confusing reading all these things altogether. And here is where hexagonal architecture come in place to help us comes to the rescue, as they say. And examiner architecture allow you to drive an application from a user point of view, from a program point of view, from an automated test or batch script. And the beauty of this is that this creating some modularization of your code that allows you also back and testing. Let's try to understand the key part of an examiner architecture. So the first part is the domain logic. The domain logic is the part where we are encapsulating what our, in this case AWS lambda should do, and therefore we are mapping where the real value lies of our workloads. In this way, we are basically using our efforts to create the logic that will allow us to retrieve information and react to requests that are coming from a client or another service. Then you have ports and ports, you need to imagine them like surrounding the domain logic and being the only entry point and output for a domain logic. So if someone else wants to interact with domain logic, has to pass through a port and vice versa. If the domain logic would like to interact with the external world, has to pass through a port. The ports, when we talk about coding, could be represented either with an interface, in the case that they're using Java, or a type language like typescript, or it could be a function like in the case of node js with es six, and that's what we are using to explore today. Then you have the third layer that are adapters. If you're familiar with adapter pattern, it's exactly the same thing. An adapter is basically a pattern that allows you to map the external interface or the external contract of a service with an internal one. And usually they are used for maintaining the encapsulated order logic for the external communication, creating the fact of an anticorruption layer between the external world and the internal world. And this layer is very useful in this case because will allow us to encapsulate the requests that are coming from the external world and translate them in a way that the business logic could digest and use it through the port. The same way adapters can be used for communicating with the external world from the domain logic. And that in that case will allow us to do, let's say other interaction. For instance, we have primary actors, usually that are the actors that are interacting directly with alumni in this case. But the exact architecture, it could be, I don't know, another service, it could be a front end application, it could be a queue. And all of these are primary actors because they are the ones that are triggering this case, our lambda, and forcing it to do something. On the other hand, we have secondary actors and secondary actors are the ones that are interacted by the lambda. So it could be that the lambda has to retrieve some information from a database, therefore it has to query the database or it has to call a third party service or even send, after computing some data, sending those information into a queue. All of them are secondary actors, because are the ones that are used by, in this case, our AWS client. Now that we understand this part, let's try to understand the benefits and drawback of this approach. First of all, the business logic is agnostic to the external world. What it means is basically we can change and evolve our business logic without caring too much how other things are communicating in the environment. As we will see in the example, you will see that the code of the business logic is completely decoupled from the interaction with the database, for instance. And in that case it means we can swap database easily if needed, or even change the way how we are interacting with the database. The other thing is the business logic is independent from external services. So if we need to change the way how we interact with the infrastructure, or even change the infrastructure, it's not going to matter. Because of this modularization and this encapsulation, testing became easier because we can test atomically part of our AWS lambda without any problem. And finally, we reduce the technical depth because we are encapsulating very well a different part of our application. There are also some drawbacks as everything in this case we need to build more layers upfront. That is not, let's say immediately a bad thing. It could be also an opportunity that we can use in order to structure properly our project. The same for the loose implementation details around the business logic. Examiner architecture doesn't provide a strong or opinionated path for structuring your business logic, but that again is an opportunity that we can use in order to structure our workload in a way that is sensible for our context and for our teammates. So if by now you are thinking, okay, why examiner architecture up to now we always discuss about layers, but that's a valid question and the answer is coming directly from Alistair cockboard. That is the creator of this architecture. The samurai architecture or the exagome was mainly used AWS a visual effect. What they realized is that it's not enough having let's say some layers or a rectangle for expressing all the interaction that a specific layer has, but having an XFL provide more surface visually for adding new interaction. That could be if you want to describe the interaction from multiple entry point, or interaction from database and caches and so on and so forth. Okay, so I prefer a demo. Just to give you an idea on how these things are interacting altogether. The demo is fairly simple, is a lambda that is called stock converter that is triggered by a request that is coming from the client. Then we have a dynamodb table that is used for retrieving a stock value. And then this stock value is kept in memory for the lambda. The lambda then is calling a third party service for retrieving the live currencies. For the live value of the currencies of specific, let's say currencies around the world, apply them to the stock value, and then return back the response to the client. So very simple, nothing too complicated. But just with this example we will be able to see the benefit of this approach. So if we want to visualize what's going to happen. So the first thing is there is an HTTP request that will be picked by can adapter. The adapter will communicate with the port for communicating with the business logic. And therefore basically it's retrieving the adapter in this case is retrieving the stock id and then passing that the business logic through the port. The business logic then takes the IP, communicates through the port to an adapter, and in that case the adapter is communicating with DynamoDB where we store our value of specific stock. Then the business logic is communicating again with another ports and the ports is communicated with can adapter for retrieving the live value of the currencies. When everything is finished, we return back to the response and therefore we fulfill our execution in our lambda. Okay, so let's jump to some code. This is how I structure the project. As you can see here, I have the adapters folder, domain and ports. Those are the three concepts that we have seen before when I was discussing about the anatomy of XML architecture. The interesting bit here is that the entry point that is this app js that you can find outside all the folders is the only thing that it does is retrieving the stock id that is present inside the rest API that was consumed by the client. It's receiving the stock id and the first thing that it does, it doesn't do anything, as you can see, and doesn't provide any logic outside our architecture. The first thing that it does after retrieving the stock id is passing the stock id to an adapters. This adapter has a function called get stock request, and what it does is retrieving the stock id and passing to a specific port that is used for communicating with the business logic. The other thing, everything is asynchronous. In this case I'm using node js with DS six, and here I'm preparing the response in case that I fulfill the logic and also I prepare an error. Obviously in this case I omitted a lot of details around metrics loggings, mainly to focus more the example around how to structure Mexagon. So here we go to the port and in the port, in this case we are using s six and therefore we don't have interfaces. So we can easily use a function in order to communicate from the external world to internal worlds, basically from an adapter to the business logic, and in this case the port. What it does is literally mapping the request from the adapter to a specific function inside my business logic. And when I go to stop where that is my business logic here I can see immediately first the currency that I want to use. Potentially it could be an environment variable. In this case I just map as constant inside my logic. Here the first thing that I do is retrieving the stock id. Then I'm sending the currencies that I'm looking for and to a third party service. And then I apply the value that is coming in euros to all the currencies and return back the response to the client. But the interesting part is here. As you can see, the business logic is not aware if we're using Panama DB, if we're using Aurora, or if we're using memorydb, anything. It doesn't matter the database for the business logic, because what it matters is that I'm looking for the value of the stock, the same for currencies, currencies, it doesn't matter which is the service I'm using, it doesn't even know the business logic which is the service that I'm using. So let's try to explore a bit this concept. Let's go with the repository first. So as we said, the business logic is calling a ports now that is calling an adapter once again, the port is nothing more than a function. And when I go to get a stock value here, I'm mapping the logic to communicate with Dynamodb. The interesting thing is that everything is encapsulated here. So if I need to make a change on dynamo in the way I'm querying in the schema, or even in the way of I want to potentially store value, that is not the case in this example, but potentially in a crud implementation I could. The only thing that I have to do is go into the right adapter and start to atomically make a change or improvement. When here I have retrieved the item in action, I return back the information to the business logic. Okay, let's go back to the business logic. The other using is the currency. So I want to retrieve the currencies in this case. Once again I have my port and in my ports I'm calling can adapters. The adapters. What it does is very simple, is calling point to point this API, and this API is returning a payload that contains some values. Everything is very simple. We deploy this in production and let's assume that these workloads start to have a certain amount of traffic. That is quite common if you have a very successful application. Now there is a new requirement. You start to see that there is some throttling in the third party service because at some point you have too many requests and because you have implemented the code in this way where you go point to point and every request goes to retrieve the real time value of this currency. It's not going to case very well. So now we need to think about how we can improve this. And there is a specific pattern bubbles in my mind that is called a cache aside pattern. So potentially what we can do is go into our adapter, sorry, go into our port and create another adapter that in this case is currency converter with case. So we can use a cache aside pattern. What it does basically is first looking into a cache if there are some value available, and if they are, they return immediately the value directly from the case. Instead of going to inquiry and consuming an API from a third party system, this basically will offload all the requests or bus journey of them from our application to a third party system. And again you are going to have the similar result because you have data that are available inside your cache. In this case I used elasticache, that is another AWS service that allows you to use redis, or in this case I'm using redis for creating a cluster where I can store retrieve first information if they are, if there aren't, I'm just storing them. So as you can see here, I have the logic. I'm using a normal node JS redis client and in this case I'm just looking. I have like an id that's currencies. If there are some can array of values for those currencies, I will return back immediately. And I don't even go through the request to a third party service if there isn't anything. I first retrieve the information on the third party service and then I immediately store this response with an expiration time of 20 seconds in the cache and then I return the data to the piece of logic. As you have seen here, I didn't have to change anything apart from my port just to change basically the import that I need to do. But that is more peculiar for Es six and JavaScript. But if you're using another type language, potentially the thing is you just need to be compliant with the interface that you have created and therefore through dependency injection you would be able to just create a new adapter or change the existing adapter that you have. I prefer to, because it's very atomic, it's very small, I prefer to have two adapters. So I can also, let's say revert back quickly if I need to make some tests and make sure that everything is working correctly. But the beauty of this, that is, atomically we were change only one file and the rest of the application remained exactly the same because we are not creating the same contract. But moreover, because the modularity provided by this approach allowed us really to be specific on the thing that we need to change. Now let's assume that we have another example that we want to pursue. Let's assume that this team, instead of starting straight with AWS lambda or a serverless workload, they started with a container. Maybe it's running on ETS or elastic container service ecs. So in this case our application, as you can see, we have the same structure, we have exactly the same files also. And the interesting approach of this is that when we map our endpoint in this case is a gap with passing this code stock and we pass the id of the stock, this is exactly the same entry point that we have in our lambda. What it means is that potentially the moment that we have a container that has maybe a crud operation for creating, updating, deleting and reading some information from a database. If we structure our container in this way, it becomes easier then to refactor and extract ports of our application into a new compute layer. That is great because it means we can really leverage the power of the cloud provided for moving our logic across multiple components based on the volumetric that our service is used to have. Okay, let's go back to the slide now. Now obviously someone can think, okay, that's great, I can test better, I have good modularization, I can start to have my code in a really great way. But what about anything else? What I'm gaining with exceptional architecture, I found some use cases that I believe are interesting to think about when we want to use this architecture. So the first one is testing because we are modularizing everything. We can really create different tests potentially based on tests for adapters and tests for business logic. And maybe probably more often you are going to change to your business logic more than the integration with the database. So in that case you can even set up some optimization in the way how you are running your test in CI CD. But even in your development environment where you are testing more often, maybe the business logic, and then when you have to test in automation and adapters on integration with external world, you can do that. But thanks to this approach, this very modular will allow you really to make this reasoning and also optimize your feedback loop when it comes to testing catches and pattern. We have seen that. So we have a service that is hammering our lambda in this case, and then the lambda is making with the database. Obviously sometimes all the queries that we have are quite common and are requested by multiple customers. So in this case what we can use is having a cache and they use this cache site pattern where first we read from the cache and then if the cache is expired or evicted, we can go to the case. The interesting part of this approach is that not only the cache button can be used, it's one of the pattern. It can have you have a read through cache or a write through case, and it's completely up to you to handle that. But the beauty is that if we want to change for any given reason specific integration with the database, the only thing we need to change AWS long we maintain the same contract between the business logic and the adapter is at the adapter level. So in this case, the only thing really that we need to change is at the adapter level for improving the performance of our application without touching the rest. Another example could be a change in trigger. Imagine that you have like a workload that is, let's say currently working with API that is triggering a lambda function, and at some point you realize that that is not needed anymore. You have a lot of traffic, you don't have to handle everything synchronously. You can handle that asynchronously, so that what you could do, instead of having a direct connection between API gateway and lambda function, you can have an API gateway that is running to AWS sqs, so a queue, and in that case the lambda is triggered, retrieving a batch of elements at the queue and then doing computation and so forth. On the other side, the client can start to pull an API for retrieving the computation that is done by the lambda. That is a common scenario when you have, let's say you can work with a venture of consistency, or you want to work in a way where you want to reduce the strain to your service and rely on the fact that infrastructure can handle that. Another approach is service migration. Imagine a situation where you have your application and you're using maybe a self managed database. In this case, let's assume MongoDB. Let's assume that you have MongoDB running on can ec two instance, and at some point, yes, you need to maintain, you need to update the cluster, you need to make sure that it's up and running and so on and so forth. But that is, let's say, taking a lot of time for maintenance. What you cloud do is that say, okay, listen, I'm not here for maintaining database. I'm not doing anything crazy with my database. I just want to migrate my data to a managed database. So in this case MongoDB. In AWs you can use documentb that is compatible with Mongo. And in order to do so you can migrate the data behind the scene and there are the migration service that would allow you to do so, but also the computation layer. You can even apply a logic where you can maintain for a certain period of time both databases and the adapter level. You are just, let's say querying primary on document DB. And then if you don't find specific record, you can go to MongoDB. The beauty of this approach is that you can even apply a branch by abstraction and slowly but steadily migrate away from MongoDB or submanage database to a new one, and that all the logic for doing so is encapsulated inside an adapter. Once again, all the rest of the logic and the lambda is not going to be change, it's not going to be affected because you are encapsulating very well using examiner architecture. Another approach is web application modernization. And when you have, for instance a modular moderates, therefore you identify some domains at your business logic, you may want to migrate from your instance to containers. So in that case you want to use microservices. And if you use exact architecture, you can slowly but steady retrieve portion of your domain and therefore bounded context encapsulated microservice and slowly but steady migrating your module only in distributed system. Moreover, you can do the same moving from microservices, therefore from a container to lambda. And in that case it's very interesting because you can have, let's say a decision on how you want to handle that. Imagine for instance that you have a cloud operation inside the microservice and you want to migrate to lambda. You don't have to migrate every single operation in a unique lambda. It depends from your world metric. Be pragmatic there. You can potentially say okay, in my interaction with a service I see that they have a lot of read but not many deletion and creation or update of a record. So what I can do initially for doing a quick move towards a serverless option is taking the read, put a logic and put inside the lambda and then the rest you can put inside another lambda. So you will end up with two lambdas, one for handling the reads and one for handling all the rest of the operations. The beauty of this approach is that you can iterate inside your workload and your architecture slowly but steady. And because you're using a modular approach, you will be able to extract pieces from a lambda or from a microservice very quickly without having too many headaches when you want to do so, another approach could be hired strategies. Imagine that you have some workload that has to live on prem and on cloud. And in that case, what you could do with a cyber architecture is something like that. You can have your cyber architecture that is running on AWS with lambda, and then you may want to use knative that is running maybe in AWS or on Prem, and the business logic will remain the same. And that's the other cool thing, because in this case you can only change the adapters, therefore the environment. Because the piece of logic is well encapsulated and segregated behind force, there is no issues as long you are maintaining the same contract between the adapters and the piece of logic. And the beauty that you're using the adapter, you can manipulate the request or the response from a third party service in a way that you maintain the same contract between the business logic and the adapters. So you write once and you have the possibility to have your business logic spread and tested everywhere. Finally, there is a very futuristic approach that is called a Petalit architecture, that is leveraging also executive architecture. Petalit basically is this idea where you maintain a microservice implementation for your development space, but then when you deploy, you deploy a sort of modular model in your infrastructure. In this case you are going to reduce the distribution or distribution system, but you have the benefit of modularize different things. And there is a library currently that is in node js that is trying to achieve that leveraging. Also the possibility to load at runtime portion of the logic of your microservices, or in this case method architecture. I think it's, let's say, still early days and there is a lot to digest on that side and also to see the drawback and the benefits. But I thought it was interesting to add in this talk because you can see how exciting architecture, despite it, only a new concept is evolving through different and used through different architectures. Obviously excavator architecture was introduced in 2005 and since then there were quite a few changes. So later on there was introduced the onion architecture and right after the clean architecture, both of them are based on examiner architecture. And what they do, they solve the problem of having a more opinionated way to structure the business logic. So it's very important that you remember, if you need to structure the business logic further, you can use hexagonal architecture that are built on top of the concept of eTc architecture. In my opinion, I believe that if you're using lambda correctly, if you divide your domain correctly, it's more likely that you don't need to structure even further business logic, because the cognitive load inside a specific lambda is very small and it's easy to maintain and manage properly. After a bit that I'm talking about, that probably you're asking yourself, is it the definitive architecture that AWS recommends for working with lambda? But the answer is, it depends. As usually in architecture, the context is king, and based on the context you take these kinds of decisions. And therefore my suggestion is, if you have a workload that has to evolve and change often, please try to adopt this kind of architecture, because that will allow you to evolve your code without the risk to introduce too many issues inside it. On the other side, if you have something workload that has to be like maybe a POC or something that has to leave for a short amount of time, or a very small logic that I don't know, just has to retrieve some JSON from a third party system or something like that probably is an overkill. Therefore be pragmatic in your decision. But bear in mind that this is a very solid option for evolving your workload, especially when you work in system for a long long time. So to prop up what we have seen today is that separation concern is a key requirement for workloads now on the cloud and XML architecture provide a really strong separation concern. The infrastructure is totally decoupled from the business logic and we have easy to test an easy test path for exaggerating statue because of the modularization and the strong separation cluster. And finally you can use this approach for not only structuring your code but for many use cases in your day to day AWS a developer that are definitely simplified. In this slide you can find a lot of let's say link. And also I wrote an article that's called developing evolution architecture with AWS Lambda that basically walk you through the code example and even provide a code example that is public on GitHub and you can find link in this slide. Thank you very much for your time. I hope that you enjoyed the session. If you have any questions you can draw me a line that is on the bottom left of this slide. My personal email. Feel free to to contact me and I hope that you have a great rest of the office. Have a nice day.
...

Luca Mezzalira

Principal Solutions Architect @ AWS

Luca Mezzalira's LinkedIn account Luca Mezzalira's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways