Polylith — a presentation
This is the text version of my 20 minutes long presentation about the Polylith software architecture that I gave in three cities last month. You can also find a screencast here.
This is a service:
The typical way of organising code today is by putting all the code needed into one place. We also try to structure the code into domains and put good names on them.
If we implement this in an object oriented language, each domain would live in a namespace.
And each box would be an object.
But let’s say we get performance problems in the payment domain and that scaling out could be a good solution.
Then we create a new service, add some code to expose it, and copy the paymentrelated code into it.
Copying code can be both time consuming and boring, but that’s not the only problem. Here we have also duplicated code, which can become a maintenance problem, because now we need to maintain the same code in two places. If we find a bug in one place, we have to remember to update the code in other place too.
But what if I told you that I have a solution to this?
What is Polylith?
And yes, the solution is the Polylith software architecture!
Polylith allows us to create software by combining system level building blocks.
It’s free and open source, mostly for the backend and language agnostic and can therefore be implemented in almost any language.
How does it work?
In Polylith, the code is organised into components.
If we implement a component in a functional language, it could look something like this:
Here we have a set of functions that live in the interface namespace (payment.interface in this case). The functions in the interface are the only thing a component exposes to other components.
If we implement this in an object oriented language, it would look very similar. Here, all methods would live in the Payment class and we need to remember to declare them static to turn them into functions.
The functional way
Polylith has a functional approach to software. Functional code is very focused on processing data and getting things done, often in small steps.
In Unix for example, we can pipe results by separating a number of statements with| characters (vertical bars).
Think of the functions as pipes. When we put several components together, we just extend those pipes. The focus here is more on what to do and less on what things are, as in object orientation. This approach reduces the amount of mutable state that we need to handle, which is a good thing.
Let’s say we want to use a component from some other part of our system. Then we start by importing the Payment class which will give us access to all the functions that live in the payment component.
When two or more components are put together into one place, they will automatically “connect” via their interface namespaces. It’s just code talking directly with code. No configuration, dependency injection or other magic needed!
The code is also very readable. In a functional language the functions would live in the payment.interface namespace and we could do something like ”import payment.interface as payment”to access the functions via the payment reference.
There are several benefits of using components.
Here are some of them:
- A component is small. Something between a class and a microservice in size.
- A component is easy to reason about. Not just because it’s small, but also because it has a descriptive name and a clear set of functions that it exposes.
- Components are like Lego bricks. You can think of them as code snippets that you can move around and combine with other code snippets with almost no restrictions. This can be done because they are not associated with a place like a service and they are always connected via their interface. This makes them decoupled, composable, replaceable and the perfect building block!
We have another building block also, that we call a base.
A base is similar to a component but without an interface. Instead it exposes a public API. All it does is to delegate to other components. You never put domain logic into a base, it’s just a thin layer responsible for delegating incoming calls.
This is very convenient, because if you want to move from e.g. REST to lambda functions, then you just need to replace the base and delegate to the same set of components.
Let’s perform the same refactoring steps as before, but now with components.
In Polylith, this can be done very easily. We just need to create a new base to expose the service and specify what components it needs.
Then we replace the old Payment class with PaymentRemote and let it delegate to the new service. Because both components has the same interface, the surrounding code in the original service will not be affected by the change.
Polylith gives us a really effective development experience where we can work with all our code from one place so that we can navigate, refactor and debug our code. This gives us a really fast feedback loop and is a joy to work with, not the least if your language has support for a REPL
Another great thing with Polylith is that it gives us a really flexible deployment experience. A unique thing with Polylith is that it allows us to postpone decisions on how to execute the code in production.
In Polylith, we only introduce new services when we need them, and it takes almost no time, because we can reuse all the components that we already have.
In Polylith the development environment and the production environment are separated. This allows us to optimise the development environment for productivity while we can optimise production for non functional requirements like performance!
Keeping them separated is why Polylith is so great to work with!
But how can the same component be used in more than one service?
This can be solved in mainly two ways.
One way is to use symbolic links.
Here we have three top directories, components, bases and services. For each service, we add a symbolic link to each component and base we want to use.
If you aren’t familiar with symbolic links, they are just place holders for files or directories. This will fake that the code lives under each service, but it’s just an illusion and a way to introduce reuse!
The other way is to use a configuration file.
Here we add a a section in our build file where we point out all the src folders for the components and the base we use.
Notice that we didn’t have to delete, copy or duplicate any code here and that the development environment could be left untouched!
All this sounds really great, doesn’t it?
But does it work?
Polylith in production
And yes, it works! The reason I can say that is that I have used Polylith for three years in production, and not just me but other people and companies too.
Here we have a number of example systems that are used in production systems today. They use different technology stacks, some uses REST and other uses lambda functions, but what they have in common is that they use Polylith as a way of structuring the code.
The reason Polylith is so great is that it makes the code easier to reason about, easier to change and easier to test. It also increases the quality of the code and if that wasn’t enough, it also makes you as a developer more productive!
All together, I think we can state that Polylith works!
Let’s summarise what we have talked about.
- Keep it simple — use simple Lego-like building blocks
- Work fast — from a single development environment
- Have fun — because playing with Lego is super fun!
Who made this?
I came up with the idea, but we have been a whole team working on this together, me, Furkan and James!
If this sounds interesting to you, go here and read more. We have high level documentation, videos, an example application and more!
Written by: Joakim Tengstrand
Relaterat / Kontaktpersoner