Polylith lets you postpone the decisions regarding optimization and architecture, until you really need them. For example, when you begin a new project, you don’t need to start with multiple Microservices just because you have some concerns that might arise in the future. Everything I’m going to explain below is pretty straightforward and you’ll get all the benefits (code navigation, refactoring, etc.) of developing a monolith, while you are creating an ecosystem.
I’ll try to explain possible steps in a project, where you start with a monolithic approach and gradually split different parts into Lambda functions (this will be very similar if you choose a Microservices approach). I’ll use the RealWorld example as the starting point.
The RealWorld example is deployed as a monolith with one single system. Here is how it looks like originally (in both production and development):
All the components are packaged into one single system and users of the system can reach to functionality through the
Let’s say we want to extract the
profile component into its own system, and deploy it as a Lambda function. I’ll explain three different alternatives that you can choose from, according to your needs and specifications. In some cases you can also start with alternative 1 and refactor it to alternative 2 and 3 gradually when you see a need.
You can create a system with a
profile component and other components that
profile depends on. In Polylith, systems only keep symbolic links to the components they are attached to, so it won’t cause duplication or isolation problems. When you run
build command in your workspace, all the affected systems will be built. The ecosystem after the split will look like this:
The advantage of this approach is that you don’t need to pay the cost of making API calls between two systems in order to get
user component’s functionality, in the newly created profile system.
Each system will have a copy of necessary components’ code in it. The advantage you get from this kind of split, is that the load will be distributed over two systems. Also notice that this kind of split is very easy to do. You only need to add the
profile-lambda-handler base to your workspace and create a new system. I’ve explained how the code would look like in my previous comment.
Now let’s have a look at the development environment. It will look like this:
Here I added a base with the name
development-rest-api to combine two systems into one, on my local development environment. This is only necessary if you want to simulate the calls to the endpoints on your local machine. Even without
development-rest-api, you’ll have one REPL in your local since everything lives in one single project. I needed
development-rest-api in one of my projects since I wanted to run both backend systems locally to test my frontend applications. Please note that
development-rest-api is not included in the artefacts of any systems we build. It’s a helper base used to improve the development experience.
This alternative extracts
profile completely out of the original system (monolith) but still includes dependencies of
profile in the newly created profile system. This way, you don’t have to make unnecessary API calls, while still getting the development nirvana. In this alternative we have two systems:
Here we only moved
profile and its dependencies to our new system. Also notice that the
profile component is removed from the first system as well (different from alternative 1). In this case, when the
article component needs some functionality from
profile, it has to make an API call to the profile system. For these API calls, we introduce a new component named
profile-connector. In the first system,
article calls a function in
profile-connector which then makes the API call. Similarly, when the
comment component needs profile, it uses the same
profile-connector to make the API call.
This may seem like we are adding some extra complexity by putting API call functionality into a separate component (
profile-connector), but that is what gives us the development nirvana experience in the end. Also, while creating a better experience, we are not introducing any magic, everything is still just code, that you can follow. Let’s have a look at the development environment:
Here as you can see, we have replaced
profile-connector with the
dev-profile-connector. Both of these components implement the same interface. The difference between them is that
profile-connector makes the API calls, whereas the
dev-profile-connector directly calls the functions in
profile. This also makes it possible to write tests without mocking out the API calls, and allows you to test the actual functionality.
So, in your development environment, the ‘dev-profile-connector
will run and eliminate the API calls. But once you build your _system_ into an artefacts,profile-connector` will be included. The workspace interfaces allows you to use different components in production compared to the development environment, thanks to the workspace interfaces.
If you want to keep your services completely isolated without code from other domains, or if you are concerned that your Lambda function executable file size is growing to a point where it’s not fast enough to run (or any other concerns you may have), you can also split
profile from the rest of the system like this:
Here we only moved
spec components to our new system. Also notice that,
profile component is removed from the first system (different from alternative 1). Also, we didn’t include
user component in profile system (different from alternative 2). In this case, when
profile component needs to call
user component, it needs to make an API call. Likewise, when
article component needs
profile, it has to make another API call. For these API calls, we introduce a new component per system. So,
profile calls a function in
realworld-connector which in turn makes the API call. Also in the original system,
article calls a function in
profile-connector which then makes the API call.
Let’s have a look at the development environment:
Here as you can see, we replaced
dev-realworld-connector. Same as I explained in alternative 2, both of these component pairs implement the same interface.
Another thing I want to mention about the components
spec is that in a traditional Microservices or Serverless projects, they would probably have ended up as libraries or duplicated code. Here, with the help of Polylith, they live as code and as soon as you change something in them all the other components and systems will be updated accordingly.
I hope this answer can give you an idea about how you would develop with Polylith in your projects.