The Foundations of Software Architecture

Understanding the Notions of Dependency and Coupling

Nash
11 min readMay 2, 2024

Why do we talk about software architecture? Why do we mention best practices, design patterns, abstraction…? What is software architecture?

The goal is to maintain our code’s scalability. Poorly structured code gradually becomes more difficult to maintain and evolve. The more code and functionality the software contains, the more these building blocks intersect, and any change in one of them can/may cause problems in another.

Features by years

The above graph is a minimalist representation of the number of features developed per year, depending on whether the code is more or less well-structured. It’s a representation; the actual numbers depend on the team size, project complexity, their skill level, etc.

What we should take away is that over time, the number of features developed by the team will decrease if the code isn’t well-built. Mainly because poorly architectured code will generate more bugs with each code modification, and consequently, the time spent fixing these issues is time taken away from developing features. Moreover, fixing these problems can itself generate others, and so on.

Conversely, well-structured code may be slightly slower to start, due to more time spent thinking about the application’s foundations, and construction time may be slightly more costly, although generally the difference remains minimal. But then, as problems occur only locally, they are easier to find and correct.

So, what elements should be considered to regard our code as “well-structured” and benefit from these advantages?

The Notions of Dependency and Coupling

Software is a set of functions, classes, pieces of code communicating with each other to accomplish great things. The more these code blocks communicate with each other, the more they are coupled, dependent on each other.

The more dependent these elements are, the more a change in one function can lead to unexpected changes in another function that uses it.

Simple dependency between two class

In this example, Class A has a method “doSomething,” to perform this task, it will use the method of Class BhelpDoSomething.” We have thus created a dependency between A and B, now a modification in B could bring an unexpected change in A.

Here are the types of dependencies:

Using new()

public class A
{
public void DoSomething()
{
var instance = new B();
b.helpDoSomething();
}
}

Instantiating a “complex” type oneself (meaning non-primitive types such as int, string, boolean…) creates a dependency between A and B.

Using a static method

public class A
{
public void DoSomething()
{
B.helpDoSomething();
}
}

In this example, we consider “helpDoSomething” as a static method of B. Thus, there’s no need to instantiate B to use this method. However, this doesn’t avoid dependency entirely. Ultimately, the new() keyword acts somewhat like a static creation method as well. Here, if the “helpDoSomething” method of B is modified, the result is the same as with the new keyword.

Dependency Injection

public class A
{
private readonly B instanceOfB;

public A(B instanceOfB)
{
this.instanceOfB = instanceOfB;
}

public void DoSomething()
{
this.instanceOfB.helpDoSomething();
}
}

In this scenario, we no longer use new or static methods. Instead, we inject the dependency (via the constructor). In the example below, since B is a class, it doesn’t bring us much; we still depend on B, but we will see later that this technique, with some modifications, can have many advantages.

System Dependencies

public class A
{
public DateTime GetToday()
{
return new DateTime();
}
}

This case is a bit more particular. We have a dependency via new, a static method, or even injection, but on a primitive class of the system. However, this class, unlike most others (like string, int, boolean…), has a changing default value.

The problem is that we don’t know the system’s state and we don’t control these values. In itself, the dependency is weak (because it’s a primitive type), but it makes the class difficult to test. The new DateTime returning the current date changes with each call, the same goes for Randoms, Guids, etc. Without realizing it, we become dependent on the system (which calculates the date) and not only on the language/framework.

We’ll come back to this type of dependency later.

Nested Dependencies

Dependency tree between multiple class

Let’s complicate the diagram from earlier between A and B a bit. Now, B also uses other dependencies, as its task is complex; a service contains part of its code, and a repository allows data to be saved via an ORM.

Since A depends on B, A also depends on the service, repository, and ORM. Thus, A has a total of 4 dependencies, even if it believes it has only one.

We understand that as the code grows, this dependency tree will grow:

Complexe dependency Tree

The more classes are nested and reused, the more a change can have a big impact. For example, in this scenario, a change in the Repository can lead to changes in A, B, C, and D! A change in the ORM too!! A change in Services can cause disruptions in A, B, and D!

Dependency Zones

Now that we have a better understanding of dependencies, let’s try to represent this with a two-axis graph. The number of dependencies of a class and the number of classes dependent on it:

Graphical representation of dependencies

Each point represents a class, positioned according to its number of dependencies and the number of classes that depend on it.

For example, our class B from the last diagram would have 3 dependencies (Services, Repository, and ORM) and would have 2 usages (Class A and Class D).

Now, we will create zones on this graph, representing the points of attention to have:

Zones of dependencies

We have outlined 3 zones:

  • In red, the suffering zone: our classes have many dependencies, and many classes depend on them. They are therefore susceptible to be impacted by numerous sources of change, but also they are likely to impact many classes. Any modification in this zone becomes complicated.
  • In orange, the uselessness zone or “Ports”: the classes in this zone are not used by others, which means that either these classes are useless because no one uses them, or they are the entry points of the program, the “Ports” to use the terms of clean architecture. In this zone, it is not so serious to have many dependencies because changes here will not be reflected on other classes. Nevertheless, the more we reduce dependencies, the more we reduce the risks of breaking this class.
  • In green, the comfort zone: in this zone, our classes have few dependencies, and few classes depend on them. These classes will be easily modifiable and will have little impact on the rest of the code, so they are much safer to modify.

The ideal and maximum number of dependencies and usages depend on each case, but generally, it is considered that we should not have more than 3 dependencies. For usages, it’s a bit different; depending on the nature of the class and the type of service rendered, it is possible that it may have many, but the ideal is still to limit usages.

The more dependencies a class has, the more it will need to be updated regularly when these dependencies are updated.

A class with many dependencies is said to be fragile because it can break if one of its components generates an undesired behavior.

The more a class is used by others, the more changes on this class will generate modifications on its callers.

A class that is heavily used is said to be stable because changing its signature becomes complicated.

It is important to understand these two notions well. Moreover, here, the term “stable” could be taken positively, considering that if it is stable, it is good. Indeed, it’s not necessarily bad, but in reality, making modifications on a stable class quickly becomes costly and potentially heavy in consequence.

The Third Axis

It is normal to have classes in each of these zones! We cannot escape the suffering zone!

Considering this, the objective of good software architecture will be to know what to put in which zone. What type of class can/should go in the comfort zone? Or the suffering zone?

A third component to consider, in addition to usage and dependencies, is the reasons for change, or rather the frequency of these changes, although generally linked to the number of reasons. If my class has few reasons to change, then it is ultimately not so bothersome that it is in the suffering zone; after all, we will suffer little because we will go there little. If it has few reasons to change, it may be a pity to push efforts on strong decoupling to try to be in the comfort zone. If the effort is low, of course, take advantage of it, but if the effort is significant, then perhaps it is better to accept being in the suffering or near zone.

Conversely, if my class has many reasons to change, then it is imperative to try to place it in the comfort zone.

I am not talking about the orange zone because even if it deserves a little attention, it is either ports (like GUI, web controllers, background services, etc.) over which we ultimately have little control. Or dead code, which we can therefore delete.

Limiting usages and dependencies

Now that we know where to apply our efforts, let’s see what solutions there are to break the dependency cycle. As we saw earlier, the larger the program grows, the more the nesting and therefore the dependencies grow.

Using interfaces to break the cycle

Here I am talking about interfaces, just as I am talking about classes because I mainly work on object-oriented programming, but if you are doing functional programming, classes can be functions, and interfaces can be function pointers or delegates.

Usage of interface to loose couple

Here we find the previous diagram of dependencies between A, B, and D. Only this time, A and D no longer depend on B, but on an interface. What does this change? A and D now have only one dependency, the interface! They are no longer associated with B and all its sub-dependencies!

Similarly, B now has only one usage, implementing the interface!

So, we have reduced the number of dependencies of A and D and decreased the usages of B! It’s a double benefit! In practice, our classes A and D will have to use dependency injection. Remember at the beginning of the article, we saw the different types of dependencies. Since it is an interface, our class cannot do a “new()” on it or use it statically. So, we will have this interface injected:

public class A
{
private readonly InterfaceB instanceOfB;

public A(InterfaceB instanceOfB)
{
this.instanceOfB = instanceOfB;
}

public void DoSomething()
{
this.instanceOfB.helpDoSomething();
}
}

In this scenario, A no longer has knowledge of B and therefore its dependencies. It only knows about a contract allowing it to perform actions.

Of course, at some point, someone will have to inject B when instantiating A with something like “new A(new B());

But we will see this in another article; it concerns service containers, dependency injection. A system allowing to “automate” this process.

Another advantage, which we will also discuss later, is that A now uses an interface. We can imagine a class “B2”, or “MockB” to offer other implementations of the contract. Thus, A will still work as expected, even though B is implemented differently!

Module Definition to Limit Dependencies

A second solution to limit/break the dependency cycle is module creation.

A module is a set of classes; generally, we use the terms module or package interchangeably, meaning the same thing. It is often considered that a module must be publishable independently, like a DLL, a .Jar, or something else. This is indeed true, but we don’t have to go that far to decompose our code into modules.

The idea of a module is to define an API, in the pure sense (and not web), meaning to offer a usable system interface by the code. Roughly speaking, methods!

Imagine a class that can serialize or deserialize an object to JSON. This class would have two methods, serialize and deserialize. It can be complex to perform these two operations; the code of our class could be huge, difficult to read, to maintain. We could imagine extracting sub-concepts in the “serialization domain.” Like Fields, Object, Type, Formatter, etc. If we do such decoupling, we will get something like this:

JSon serializer module sample

The JsonSerializer class encapsulates many classes; it serves as an entry point. If, for example, the Serialize method takes an “Object” type parameter and returns a string, and conversely, Deserialize takes a string and returns an “Object” (or generic type), then all dependencies are hidden!

So, we end up with a clean module, where the outside world only knows about the JsonSerializer, and the other classes become implementation details. This component could ultimately be a monolithic class or not; no one would know. From an external point of view, it seems that we only depend on JsonSerializer and nothing else.

Now, what would happen if Serialize returns a JsonObject? And then we have to do JObject.Compute() for example? Our module now exposes two classes, JsonSerializer, and JsonObject, because the latter serves as the return type and will therefore be known externally.

This is not dramatic in itself, but it is important to be aware that if our module exposes its structure, it then becomes more difficult to change its structure. As some callers will use Serialize and thus JsonObjects, they will also depend on the latter. In fact, JsonObject will become “stable,” making any changes to it complicated.

Modules should be as much as possible black boxes, taking primitive types as input and returning primitive types as output.

This is a guiding principle; it can sometimes be interesting, even essential, to extract complex types or use complex types. But the more we can limit this, the more we limit dependencies on the structure of our module, making it easy to change/improve its implementation.

If your module export many class outside, you have to consider them as stable. It’s not really a problem, but a thing you should know when designing your module.

Conclusion

It is important to limit classes in the suffering zone, especially if they are destined to change regularly. Therefore, it is necessary to limit the dependencies and usages of classes subjected to frequent changes.

It is important to know when to use an interface to break the dependency tree (not to mention the other advantages of interfaces that we will see later).

It is important to understand that the types our classes take in and out can generate dependencies on our implementation. Therefore, we must be vigilant and, if possible, limit access to the “children” elements of our module.

If you have understood all this, then you have entered the world of software architecture. The world of compromises, the world of reflection on code, communication between components, but also pragmatism.

--

--