Sharing code
Last updated
Last updated
People often wonder what Polylith brings to the table and if it's worth looking into.
To see if Polylith might be right for you, ask yourself whether these statements are true or not for the systems you work with:
I can easily split up a service in two.
I can easily share code between services without creating libraries.
I have no unwanted code duplication in my entire system.
I can easily change, find, refactor, debug, and reason about all my code, even across services.
There aren’t many developers who can do all the above, but if you can, congratulations! We still think that Polylith can help you be happier and more productive, so keep reading!
Let's say we have two nicely structured services, where each box represents some source code that lives in its own namespace with a single responsibility:
If we want to use the code that lives in the red box in both service A and B…
...we have four alternatives:
Duplicate the code
Create a library
Create a service
Use a monorepo
If we take a closer look at service A, we realise that the red box depends on the green box that depends on the purple box:
Now we not only need to copy the red box, but all three of them, and paste them to service B:
This introduces a lot of code duplication which is generally considered undesirable.
The second alternative is to create a library:
Because the three boxes were connected, we have to include all of them in the library, which violates the single-responsibility principle.
Creating a library also harms the development experience, because changes to the code no longer take effect immediately, as we need to build a library every time we make a change.
If we have many services and perhaps many teams, the risk is that we don't get round to updating the version in all the services, which means that we don't use the latest code everywhere.
Another option is to create three separate libraries instead of one, however this will make it even harder to keep the code in sync.
The longer we wait to upgrade all libraries to the latest version, the harder it becomes and the higher the risk of introducing bugs.
The third alternative is to extract the code we want to share into a new service, that lives in its own repository:
Now we add some code to call service C from A and B. This seems to solve our problem, albeit at the price of increased complexity, as we now have one extra service to maintain, and simple function calls have been replaced by network calls.
But let’s see what happens if we add a blue box to service C, which may happen in the future:
Now this piece of code can't easily be shared by the other two services, and we are thus unfortunately back to square one.
The fourth option is to use a monorepo, where the code is shared among several src directories instead of just one. Polylith uses this idea but takes composability one stage further, which we’ll soon cover.
This is what a Polylith system looks like; where all boxes (called bricks) can freely access one another ¹ via their exposed interfaces:
An interface in the Polylith world is just a namespace with the name interface ². Since bricks now all have access to each other, we have the freedom to combine them in any way, and they will automatically "connect" ³:
Libraries and bricks are used in a similar way so it's sufficient to refer to them by name (here illustrated with dashed lines). But there are also significant differences as libraries are versioned and compressed, while bricks are plain source code which can be easily changed.
If we want to divide service B into two, we can easily do that ⁴:
Another superpower is the development project from which we can work with all our bricks ⁵ and where code changes are immediately reflected in all services:
We have now reached our goal of being able to work with all the code in an efficient way, while also enabling the code to be easily split up and shared ⁶ between services ⁷.
The next great advantage is the ability to test the code incrementally.
The whole idea with Polylith is to reduce complexity and make our life easier, both when developing the code and when changing how things are executed in production.
The introduction of new high-level concepts with well-defined responsibilities in combination with a standardized directory structure also contributes to making the code easier to reason about.
All of this aims to make coding a more joyful experience, while saving both time and money.
In the next section, we will show how the code can be tested incrementally.
¹ We are only allowed to access interfaces, not the implementing namespace(s). All bricks can access each other, except the green components that can't access the blue bases. These constraints are guaranteed through the use of a Polylith tool.
² The default name is interface but can be changed to any valid namespace name, e.g. ifc. Having interface sub namespaces are also allowed, like interface.mysubns.
³ The included bricks for a project are listed by their name in a config file, which may be implemented differently depending on the chosen programming language and tooling. An interesting detail is that a brick defines which libraries it uses, but not its bricks. They are instead defined in the projects, which makes them loosely coupled and interchangeable ⁵.
⁴ We also need to add some code to B2 to make a service out of it.
⁵ As long as we have just one component per interface, which is true 95% of the time, then these components can be directly changed from the development environment. In the other 5% of cases, they can be edited in other ways, either by switching them into development by using profiles, or by directly changing the file (but then we don’t get the refactoring support from the IDE).
⁶ The introduction of interfaces allows us to replace one component for another, as long as they satisfy the same contract/interface (often just a set of functions). The use of profiles makes it possible to switch between components that implement the same interface, from the development environment. How a service is exposed/called can easily be changed by replacing the base, from e.g. REST to lambda function.
⁷ Polylith supports any type of artifact, such as services and tools.