Who

DRAFT

Dependency Graphs

Which do you prefer? 

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

Deeper

A
B1
B2
C1
C2

Wider

A
B1
B2
C1
C2

Scenario Number A

BlogController
User
Post
SqlAdapter
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

BlogController
User
Post
SqlAdapter
MongoAdapter

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. 

BlogController
User
Post
SqlPersistor
LegacyMongoPersistor
SqlAdapter
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

TweetComponent
TweetCache
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. 

TweetComponent
TweetService
TweetCache
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:

Application
Presentation
Session
Transport
Network
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. 

api_layer
service_layer
component-specific_services
component_layer
API
TweetService
UserService
TweetCache
ErrorLog
UserMenuService
TweetPageService
TweetComponentService
TweetPageComponent
TweetComponent
UserMenuComponent

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. 

api_layer
service_layer
component_layer
API
ErrorLog
TweetCache
TweetService
UserService
TweetComponent
TweetPageComponent
NewTweetComponent
TweetMetadataComponent
UserMenuComponent
DefaultErrorHandler

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.