Functions are the smallest building blocks in Polylith from which everything is created. Most communication within a Polylith system is done by using simple function calls as a way to connect the different high-level building blocks that a Polylith system consists of.
The simplicity of functions makes them fantastic building blocks for code:
Encapsulation: functions hide their implementation and only expose their signature.
Simplicity: functions have a single responsibility and don't mix nouns with verbs, which makes them fundamentally untangled.
Stateless: functions are just code; they don't contain state or instance.
Purity: functions can be pure (i.e. have no side-effects) which makes them easy to understand, reuse, test and parallelise.
These properties make functions (especially pure functions) inherently composable and testable units of code and a perfect foundation for a software architecture like Polylith.
A library is a piece of code that lives in its own namespace which allows us to pick the libraries we want to use, without getting into name clashes (but sometimes dependency hell!).
We rely on tooling we already use, that hides the complexity of solving dependencies to other libraries and the caching of files on disk.
Components are high-level building blocks, which remove the need for layers (horizontal, vertical slice, or onion) in our architecture.
A component can represent a part of our domain (e.g. cart, invoice, order, user, etc.), or be part of our infrastructure (e.g. authentication, database, log, etc.), or be an integration point to a third-party system, (e.g. crm-api, payment-api, sms-api, etc).
A base is a special type of component that only exposes its functionality via a public API, e.g. REST, Lambda, GraphQL, gRPC, command-line, etc.
A base exposes a collection of endpoints via its API, and delegates the implementation of each endpoint to an appropriate component.
A project specifies which libraries, components and bases should be included in an artifact, like a service, lambda function, command line tool, or a new library. This allows for optimal code reuse of components across multiple projects.
A development project is the place we use to work with all our libraries, components, and bases. It gives us a “monolithic development experience” with full code navigation, debugging, and refactoring across our entire codebase, and the possibility to work with our entire system in a single REPL.
A workspace is the place in a Polylith codebase where we store all our building blocks and configure our projects.
Polylith’s single development project gives us a delightful development experience; we get all the benefits of coding with a monolith (code navigation, debugging, refactoring, and a single REPL) but maintain the flexibility of deploying our components into any combination of artifacts.
The LEGO-like components simplify the design of our tools and services by giving us building blocks at the right level of abstraction, which are understandable, composable, reusable, and replaceable.
Components are inherently simple and easy to reason about; they are just code, have a clear interface, and hide their implementation.
Components maximise code reuse to the point of having zero code duplication across our entire codebase.
Projects maximise the deployment flexibility by allowing us to combine any set of building blocks.
Now, let's dig deeper into the Polylith architecture to better understand how it solves these challenges.