Who

DRAFT

Dependency Graphs

Which do you prefer?

  1. Deep chains of dependencies, or
  2. Shallow but numerous dependencies?

Deeper

graph TD; A-->B1; A-->B2; B1-->B2; B1-->C1; B2-->C2;

Wider

graph TD; A-->B1; A-->B2; B1-->B2; A--->C1; A--->C2;

Scenario Number A

graph TD; BlogController-->User; BlogController-->Post; User-->SqlAdapter; Post-->MongoAdapter;

The BlogController depends on User and Post, and they in turn know how to load and save themselves from their respective datastores.

This is fine. It’s neither deep nor wide. It’s even preferred for small, medium, AND large projects for a long time. Regardless, it’s common to separate the persistence behavior from the domain objects for more flexibility when needed.

Consider this amazing chart:

preferred?
       yes | xxxxxxxxxxxxxxxx
           |                 \
        no |                  xxxxxxxxxxxxxxxxxxx
-----------+-------------------------------------->
     time =>      early               later

It’s easy to start with this and change later. In fact, it’s usually easier to start with this. So plan to start with this and it may never change later. This is one case where taking the easy road in the beginning saves long-term cost.

Subscenario Number A.1

graph TD; BlogController-->User; BlogController-->Post; User-->SqlAdapter; User-->Post; Post-->MongoAdapter; Post-->SqlAdapter;

Well, you started storing Posts created after July 23rd in Postgres as you phase out MongoDB, and the User domain model grew to have knowledge of related Posts.

It’s time to simplify that graph.

graph TD; BlogController-->User; BlogController-->Post; User-->Post; BlogController-->SqlPersistor; BlogController-->LegacyMongoPersistor; SqlPersistor-->SqlAdapter; LegacyMongoPersistor-->MongoAdapter;

This “simplified” structure isn’t that much better but does have some benefits.

At this time the Post will have a loose coupling to its storage technology via creation date. When Posts are migrated to SQL a branch from this graph can be deleted without fear of breaking the User or Post domain models.

(We kept the domain model dependency between User and Post because that represents a real relationship in the problem & solution domains (nice to have).)

Scenario Number B

graph TD; TweetComponent-->TweetCache; TweetComponent-->TweetService TweetCache-->TweetService; TweetService-->API;

In this scenario, a simple component relies on a TweetCache to load Tweets if they aren’t already in the cache. The component also uses the TweetService directly to post Tweets and fetch Metadata.

Here, the depth is problematic.

  1. There’s a leaky abstraction - the TweetCache isn’t sufficiently abstracting the functionality of the TweetService so the component has to reach around it to interact with the TweetService directly.
  2. It doesn’t make sense for the TweetCache to both cache Tweets AND act as a proxy to the TweetService. Those two functions distinct and are better separated:

This is the kind of arrangement that causes problems from the moment it enters the codebase. Over time the leaky abstraction causes more and more complications, more and more delays.

It’s easily fixed, too. Move the auto-fetching from the TweetCache into the component turning that baked-in behavior into a composable behavior.

graph TD; TweetComponent-->TweetService TweetComponent-->TweetCache; TweetService-->API;

That’s much nicer. The implementation is easier and clearer, responsibilities of each piece is readily understood looking at the code. Changes will be easier to make so it will be less fragile. That decreases the depth and, in this case, didn’t increase width.

In this scenario, the shallow structure wins.

Scenario Number C

Layers. You may recognize the 7-layer OSI model for computer communications:

graph TD; Application-->Presentation; Presentation-->Session; Session-->Transport; Transport-->Network; Network-->DataLink; DataLink-->Physical;

The depth of this model is its strength. But beware that each layer could be implemented with either deep or shallow internal dependencies.

Here’s the example from Scenario Number B

  1. with a few more components and services, and
  2. drawn as layers.

Note that this diagram includes the unfortunate initial leaky abstraction and adds component-specific “helper” services which both (A) keep the component code to a minimum and (B) complicate things.

graph LR; subgraph component_layer TweetPageComponent TweetComponent UserMenuComponent end subgraph component-specific_services; UserMenuComponent-->UserMenuService; TweetPageComponent-->TweetPageService; TweetComponent-->TweetComponentService; end subgraph service_layer; UserMenuService-->TweetService; UserMenuService-->UserService; TweetPageService--->TweetCache; %% TweetPageService-->UserMenuService; TweetPageService--->UserService; TweetComponentService--->TweetCache; TweetComponentService--->TweetService; TweetCache-->TweetService UserMenuService-->ErrorLog TweetComponentService-->ErrorLog TweetService-->ErrorLog TweetCache-->ErrorLog end subgraph api_layer; UserService--->API; TweetService--->API; end

And here’s the same scenario, flattened using the same technique as shown in Scenario Number B. Flattening like that lets us take advantage of another technique for decoupling: using a default error handler to interface with the ErrorLog.

graph LR; subgraph component_layer TweetComponent TweetPageComponent NewTweetComponent TweetMetadataComponent UserMenuComponent end subgraph service_layer; ErrorLog TweetComponent-->TweetCache; TweetPageComponent-->TweetCache; TweetPageComponent-->TweetService; NewTweetComponent-->TweetService TweetMetadataComponent-->TweetService UserMenuComponent-->TweetService; UserMenuComponent-->UserService; end subgraph api_layer; UserService-->API; TweetService-->API; end DefaultErrorHandler-->ErrorLog

Now these dependency lines mean something. For example, the relationship between the TweetComponent and the TweetPageComponent is clear by looking at this diagram and applying a little common sense. We are now armed with information to help reason about why the UserMenuComponent depends on both the UserService and the TweetService - perhaps that’s an inadvertent dependency that could be removed. Either way it will be apparent from the code and easily fixed within this structure.

And the Winner Is

🏆

Shallow*

*Don’t go overboard. Having depth in a layered structure is good when the abstractions are right, but within one layer shallow architectures are far better.