Indirection is fundamental to software design. Creating abstractions is one common way of creating indirection. The benefits are reuse, isolating complexity, encapsulation of dependencies, and more. But what’s the cost of indirection & abstractions? Cognitive load to fully understand all of the layers of a request and limiting functionality.
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.
To illustrate indirection, first, let’s get down to the basics of having calling code (Caller) that is invoking another piece of code (Target).
Adding indirection simply means adding something in between the caller and the target.
As developers, we’re adding indirection all the time without really thinking of it. It’s not inherently bad and comes with a lot of benefits. Adding indirection is a useful way of isolating complexity, allowing re-use, and abstracting dependencies as we can have many different callers use the same abstraction.
A typical example of indirection is adding an abstraction of data access. This can often be thought to if you’re creating some type of data abstraction layer, a repository, etc.
If you’ve ever used the decorator pattern or created a request pipeline (check out my post on Separating Concerns with Pipes & Filters), you’re creating indirection.
Another way to think about indirection which isn’t directly related to application code is infrastructure. Indirection can come from a message queue, topics, load balancer, etc.
I’m going to be showing snippets from the eShopOnWeb reference application. Below is a from the OrderController that has a route to show the signed-in users Orders.
The first use of indirection in our code is using MediatR. Instead of having application code in our ASP.NET Controller, we’re using MediatR to invoke our Query Handler which will have that logic.
The second use of indirection is we’re injecting an IOrderRepository and using that to get out the list of Orders. Meaning our indirection is coming from separating data access. When then take that list and transform it into a list of OrderViewModels that is returned from our Handler.
The OrderRepository is actually adding another layer of indirection because it is using Entity Framework Core within it to get data from the database.
So the call stack from the Controller to our database looks like this:
If we removed most of the indirection it would look like this:
I’ve left Entity Framework as a layer because ultimately you’d be using some type of data access client to get to the database, regardless if that’s Entity Framework or simply ADO.NET directly.
Now I’m not implying you should remove indirection! There are clear benefits. To start with, the usage of MediatR can be benefiting from not coupling your application code with ASP.NET Core. Let your application code focus on application logic and let ASP.NET Core handle HTTP.
In the case of the repository, its purpose is to abstract the dependency on Entity Framework Core. Instead of having application logic directly couple to a 3rd party dependency, creating an abstraction of the repository allows you to couple to a type that you own (although I’ll argue later I don’t have to).
Cost of Indirection & Abstractions
The first cost of indirection is cognitive load. If you need to understand the full life of a request and everything that happens, depending on how many layers the request is passing through can be challenging.
In the example above, it’s pretty simple however you can imagine the more indirection that exists, the more difficult it will be to understand the full scope.
On the flip side, there can be the benefit of not having to worry about certain layers. Meaning you simply don’t have to concern yourself with them. Until you do.
My point is keeping indirection to a level where you have the ability to fully understand the entire request and how it pertains to the application code you’re writing.
The second cost is performance because of limited functionality. Not necessarily from a memory allocation or CPU perspective, although that’s possible, more because when you’re creating indirection through abstractions, you’re often times making your abstraction generic or a limited surface of what we’re abstracting. This occurs often when you’re abstracting a 3rd party dependency.
To illustrate this, look back at the Handler that was using the repository. Does it really need to use a repository? what effect does using the repository have?
To get all the Orders out of the Repository, it was taking a Specification. This is the specification it was using. Its purpose is to add the Where() so it’s only fetching the orders for a specific user and to eagerly load the OrderItems and then the ItemOrdered which is the actual product.
Here’s the IRepository, which if you’ve used a repository before, probably looks pretty familiar.
This is all very generic and limits the ability to really leverage the underlying data access, which is Entity Framework. Because we’re abstracting entity framework behind this Repository, we’re now stripped out a bunch of functionality that we can’t expose.
Because of this, the listing page is getting back way more data than it actually needs.
The repository is returning line items and for each line item the associated product. None of this is used within this view.
Personally, I’d rather not use a repository in this situation. Why? Check out my video on Should you use the Repository Pattern? With CQRS, Yes and No!
Indirection is something we’re constantly creating but it has a cost. Be aware of when you’re adding indirection and if it actually adding value. If you’re abstracting a dependency so you can make it more testable, then great. If the dependency is testable and you simply don’t want to directly couple to it, then that might make sense, or it might not!
Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.
- Restructuring to a Vertical Slice Architecture
- Decomposing CRUD to a Task Based UI
- Write Stable Code using Coupling Metrics