In my current project, we use Self-Contained systems to modularize large web applications into multiple applications. We were facing the question of how we should share code across our applications in the system.
The drivers for building self-contained systems
Let's talk about our drivers to use the self-contained systems architecture before we go deeper into that question.
We were facing a situation, where our customer had two legacy systems running. They are doing more or less the same. Actually, one of them was supposed to replace the other one, which has never been completed entirely. For extending and replacing those two systems we have chosen the self-contained system approach. This gives us the flexibility to maintain and migrate separate parts of the system in the future more easily.
One important part of the self-contained system architecture is to keep them as independent and decoupled as possible, otherwise, we would lose the advantages why we have chosen this approach.
Enforcing UI design guidelines
We are working on a couple of applications in this system now. The user should not really recognize that he/she is browsing multiple applications. We can't avoid page loads due to the architectural design we have chosen. But design, styles, and layout should not look different between the app.
Sooner or later, we need to think about, how we can enforce these constraints. Now, self-contained systems have a lot in common with Micro Services.
One strong recommendation when building micro-services is to not share code between the services. Sharing code increases the coupling between the services. And that makes it harder to maintain them independently. There are some companies like Netflix that purposely share code though, but I think they are the minority.
The rule to not share code is also valid for self-contained systems. We don't want to touch the entire system of systems when we, for example, update an application to a new Angular version. On the other hand, we don't want to fix bugs in code which is used from multiple applications multiple times. Or change the style in all applications manually.
Introducing versioned libraries
We need to find a way in between. Sharing code directly creates a hard dependency on that piece of code and we want to keep the freedom to prioritize and decide when we touch an application. We decided to create versioned libraries containing the shared code. It is important to make it versioned because it allows you to update this library in an application independently and on demand.
Building, publishing, and consuming an npm library is easy. So everything fine? No! For us, it introduced a completely new constraint. We have two teams and we first had to learn how we handle this library. Questions like "How do we do versioning?", "How do we handle branching?" or "How do we handle changes, especially breaking changes?" needed to be answered.
We introduced Semantic Versioning to be able to express breaking changes. We are working in a mono repo. When we create a release branch we update the major version number of the develop branch to avoid version conflicts between the branches. And we keep a lean changelog to have an overview of the changes and breaking changes we did. We also have a gathering among the developers. We call it a dev exchange. This is the platform where we discuss and announce cross-cutting concerns like our shared libraries.
Would I do it again? I'm not that sure. It definitely brings some value for us. It works quite well for things like UI components and global styles, but it also introduced an overhead we actually don't want to have.
Nothing is a silver bullet. It did not solve all our problems with shared UI components at the end. With this approach, we still need to touch all the applications when we introduce a new navigation item. But that is another story.