How Polylith helps us fight complexity.
The Polylith team loves simplicity, which is one of the main reason we chose Clojure when implementing the Polylith Tool and the RealWorld example app. For newcomers, Clojure’s syntax probably looks like a bunch of weird parentheses, positioned in strange places, for no apparent reason. However, Clojure’s parentheses solve a real challenge with communicating the structure of code. In the same way, Polylith’s approach to structuring code may appear alien in the beginning. But every decision has been carefully weighed by the Polylith team to optimise for simplicity, development speed, and developer delight!
It is no coincidence that a component lives in its own src directory, that it’s just “plain code”, that it has an interface, that it’s only allowed to depend on interfaces (and libraries) and that it lives in a monorepo. All those things are there to decouple our system(s) into small Lego-like bricks in a way that they can be shared and put together in a useful way.
Components are very similar to functions in that way but operate on a higher level. A function is composable, easy to reason about, has a well-defined interface and is fundamentally simple. A component is also composable, easy to reason about, has a well-defined interface and is fundamentally simple.
Teasing things apart so that they can be composed together is often a sign of good design. It has been as hard to convince people that Polylith is a good idea as it has been to convince them that Clojure is. Our experience though, is when they start using either of them, they will soon discover how simple they are, how fun they are to work with and how productive you become, and that’s why we love Clojure and Polylith.
This is how Polylith fights complexity:
- 2.We have only one of each component and interface, which removes code duplication and maximises code reuse.
- 3.A component is "just code" that exposes a set of functions and we avoid the complexity that comes with mutable state.
- 4.A component lives in its own place, a separate src directory. The default way of structuring code is normally to put different functionalities together in one or several places/projects. The result is that each piece of functionality can't be shared across projects. Polylith solves that problem by letting each functionality live in its own separate place, and be used everywhere.
- 5.Components have interfaces and only know of other interfaces. This decouples them from each other and makes them composable and replaceable.
- 6.The single development project helps us keep the contracts between the components in sync and stops us from introducing bidirectional or circular dependencies. In other words, it guarantees that all dependencies point in the same direction, even across projects.
- 7.A project specifies what building blocks it contains. There are two things that make this possible. First, all dependencies point in the same direction. Second, not only frozen code (libraries) can be reused across projects but also living code in the form of components and bases. This maximises composability and allows us to pick and choose what to include in each project as if they were LEGO® bricks.
- 8.Polylith separates what from how at all levels, from functions and bricks to the workspace. What is represented by function names and their signatures, component interfaces and base API's, and all the bricks in the workspace. How is an implementation detail, including how each function and interface is implemented and how to execute the code in production.
- 9.The introduction of new high-level concepts and the standardized structure makes it easier to reason about the code and reduces the cognitive load, which results in reduced complexity.
As you can see, Polylith is not only about productivity but very much about simplicity.