Modularization in open source projects
I recently came across a blog post from Shopify where they share how they are componentize the main Rails application into smaller pieces with clearly defined boundaries and loose coupling between them. This made me think about the uFeatures architecture that I proposed back when I was iOS engineer at SoundCloud, and that I naturally inherited in Tuist.
Typically open source Swift CLIs are organized in two targets, one that represents the executable (i.e.
main.swift), and another one, typically named with
Kit, that contains all the business logic of the tool. The main motivation for doing is being able to implement tests for the business logic. However, since everything lives within the same boundaries, there’s a huge risk of the business logic growing into large group of components strongly coupled, and an architecture that is hard to reason about, maintain, and evolve. It’s a good starting point, but not a good idea long-term because it’ll compromise developers’ efficiency contributing to the codebase, and complicate onboarding new contributors to the project.
As I mentioned earlier, Tuist follows the uFeatures architecture. There are Tuist-agnostic targets like
TuistSupport (inspired by Rails’ ActiveSupport) and
TuistTesting that contain utilities that transversal to all features and core utilities: abstractions built upon foundational APIs and extensions. There’s a
TuistCore target that contain models and business logic that is core to Tuist; for example the dependency graph and the models that represent the projects. This target also acts as a dependency inversion layer so that feature targets don’t have dependencies among them. Thanks to this, we can build a feature without having to build the others. This makes iterations cycles faster when working on individual features. Then features are organized horizontally. Most of them represent the different command namespaces that are exposes by the CLI. In some cases like automation commands, they are grouped under
TuistAutomation. Cloud-related utilities live in
TuistCloud. This is great for new contributors because if they want to fix or improve something in the
tuist build command, they only need to onboard on the
TuistAutomation target. Isn’t it great?
Last but not least, we have the
TuistKit that glues all the features together into a command line interface that is hooked from the entry
main.swift file. Commands are classes responsible for parsing the CLI arguments and throwing errors when they are incorrectly used, and delegate the business logic to
Service. For example, there’s a
GenerateCommand and a
One might think that this is over-engineering a project, but I’d certainly disagree. Defining clear boundaries in a codebase by leveraging Swift’s access levels will lead to a better architecture, which in turn, eases contributions and the addition of new features. Starting the modularization way after creating the project will be a hard challenge to undertake because the code will most likely be strongly coupled. We tried to do that at SoundCloud and, from what I know, there’s still a lot of code that lives in the main app that is hard to extract.
I can’t imagine Tuist being a monolith codebase these days.