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.