Some guidelines for dependency rules that make a system easier to understand and maintain.
Dec 22, 2022
Last week, we published a blog post on how you can create dependency rules between your packages. This post encouraged intriguing conversations, among others, in this HackerNews thread. These conversations mainly focused on what dependency rules make sense for many projects. In this post, we'll dive into some of those ideas and why they might make sense for your projects.
We'll present 3 different guidelines:
The Law of Demeter was proposed by Ian Holland at Northeastern University in 1987. It states that an object should only communicate with its immediate neighbors. It shouldn't depend on the inner workings of other, "more distant" objects.
The Law of Demeter suggests how objects in an OOP system should communicate. But it makes sense on a higher level, between the packages of a system as well.
Some benefits of the Law of Demeter:
Self-composed packages with few dependencies have a further advantage: They are more handy to move around. That becomes relevant if you want to reuse the package at other places of the system. Or perhaps even extract it into its own library or service. (In Building Microservices, Sam Newman provides some great insight into both whether and how to start splitting a monolith into smaller parts.)
The Gateway Pattern was first described by Martin Fowler in the book Patterns of Enterprise Application Architecture in 2002. His definition: "An object that encapsulates access to an external system or resource".
Using a gateway for external libraries provides several benefits:
On his homepage, Fowler writes that "this pattern is widely used (but should be more prevalent)". He also gives a great guideline on when to use it:
I use a gateway whenever I access some external software and there is any awkwardness in that external element. Rather than let the awkwardness spread through my code, I contain to a single place in the gateway.
Even if you don't have such a sophisticated system, considering the maturity level of both internal and external packages makes a lot of sense. Some questions to ask:
The answers may be more obvious if your system is split into several small parts, perhaps even repositories. If you have a big monorepo, you'll probably find some modules or packages which are surprisingly difficult to categorize.
Thinking in maturity levels is surprisingly helpful with the classical dilemma: Should I try out that cool & new library? Or should I be consistent and stick to libraries that pass the well-maintained test with flying colors?
For the code that processes thousands of transactions every hour, the shiny new toy might be too risky indeed. For the internal tool automating a part of your build, it might be an excellent choice.
w_t_payne points out how being explicit about maturity levels helps innovation and experiments:
It also means that there's more freedom in changing and adapting peripheral designs as you can have confidence that its stability is not something that is going to be relied upon.
Having clear boundaries and communication lines between your packages makes your system much easier to comprehend and maintain. Where these boundaries should be, depends highly on your project's domain.
However, there are some general guidelines on how parts of the system should communicate. The Law of Demeter and the Gateway Pattern are worth to follow in the majority of projects.
Check out our post Maintain A Clean Architecture With Dependency Rules and start creating dependency rules for your project in 3 steps: