Design Principles: Dependency Inversion Principle

Flip It and Reverse It

The Dependency Inversion Principle is an object-oriented software design principle that easily has the most far-reaching implications across both the underlying code and overall architecture of a software application of all design principles.

This post is part of a series on software design principles. As with all design principles, this is a general guideline, not a hard and fast rule. Following principles such as this will improve code quality, legibility, testability, and maintainability.

The Dependency Inversion Principle (DIP) is the D in the SOLID design principles acronym. While it might be the last principle in the set, it’s arguably the most important of all design principles. The DIP states that software should rely on abstractions instead of concrete implementations. While this is usually taken in the context of OOD to only mean that one simply should code always against interfaces rather than implementations, that is to miss half of the picture.

The DIP is usually conflated with Inversion of Control (IoC) or Dependency Injection (DI). Neither of these concepts has anything to do with dependency inversion. IoC is about inverting control flow within a platform so that the platform calls into registered clients, rather than clients having to call into the platform. And DI isn’t even a principle: it’s a design pattern wherein dependencies are passed to an object at the time of its construction rather than using other dependency management design patterns (such as the Service Locator pattern). DI and IoC are usually implemented together, such as through IoC service containers, but this certainly isn’t required: DI can be implemented manually, as seen by the standard approach to mocking dependencies in unit tests. While DIP, IoC, and DI can all work very well together, none of these 3 concepts are required for the other two.

For example, the following two implementations both implement DIP, but one uses DI and the other uses the Service Locator pattern:

public class ServiceWithInjection
{
    private ISet _set;

    public ServiceWithInjection(ISet<object> set)
    {
        _set = set ?? throw new ArgumentNullException(nameof(set));
    }
}

public class ServiceWithLocator
{
    private ISet _set;

    public ServiceWithLocator()
    {
        _set = Dependencies.Get<ISet<object>>();
    }
}

public static class Dependencies
{
    private static IServiceProvider Services { get; }

    public static Dependencies()
    {
        /* load up Services */
    }

    public TService Get<TService>()
        => Services.GetRequired<TService>();
}

Neither ServiceWithInjection nor ServiceWithLocator cares about the specific implementation of ISet<object> to be used, whether it is a HashSet<object> or uses some other object distinction algorithm. That is, these services only care that they can obtain a reference to a collection of unique objects, not how that object uniqueness is determined.

This allows for significant flexibility in the code. The algorithm used to determine the uniqueness of objects in these sets can be changed without having to change any part of the services that use them.

Consider the following typical SQL-based password validation service:

public interface IPasswordHashService
{
    byte[] GenerateSalt(int saltLength);

    byte[] HashPassword(string password, byte[] salt);
}

public interface IPasswordValidationService
{
    Task<bool> VerifyUserPassword(string username, string password);
}

public class PasswordValidationService : IPasswordValidationService
{
    private readonly IPasswordHashService _passwordHashService;
    private string _connectionString;

    public PasswordValidationService(
        IPasswordHashService passwordHashService,
        string connectionString)
    {
        _passwordHashService = passwordHashService;
        _connectionString = connectionString;
    }

    public async Task<bool> VerifyUserPassword(string username, string password)
    {
        using (var sqlConnection = new SqlConnection(_connectionString))
        {
            var sqlQuery = @"
SELECT
    PasswordHash
    , PasswordSalt
FROM dbo.Users
WHERE UserName = @UserName";
            SqlCommand sqlCommand = new SqlCommand(sqlQuery , sqlConnection);

            sqlCommand.Parameters.Add("@UserName", SqlDbType.NVarChar);
            sqlCommand.Parameters["@UserName"].Value = username;
    
            await sqlConnection.OpenAsync();
            
            using (SqlDataReader sqlDataReader = await sqlCommand.ExecuteReaderAsync())
            {
                byte[] passwordHash = Encoding.Default.GetBytes(
                    sqlDataReader.GetString(0));
                byte[] passwordSalt = Encoding.Default.GetBytes(
                    sqlDataReader.GetString(1));

                byte[] userPasswordHash = _passwordHashService 
                    .HashPassword(password, passwordSalt);

                return Enumerable.SequenceEqual(
                    passwordHash,
                    userPasswordHash);
            }
        }
    }
}

This password validation service mixes business logic with 3rd party vendor logic. Specifically, it hard-codes SQL directly beside the logic to verify the user’s password. If architectural direction ever decides to move from SQL to a different database technology – even another flavor of SQL, such as Oracle or MySQL – then the classes like this that contain a mix of DB queries and business logic have to be refactored.

If there are many classes containing a mix of business logic and DB queries, this refactoring process can be very time-consuming and daunting. The chances for a critical component to fail during the refactoring is high. The development team and business stakeholders might be unwilling to take on that kind of risk, even if it could greatly reduce the total cost of ownership of the software. This is an anti-pattern known as “Vendor Lock-In”, where 3rd party dependencies are not properly abstracted away to shield the application from this level of coupling.

This class is also difficult to test, since it creates a SQL connection. Tests would need to be run in an environment with an available live SQL server, and connection strings must be provided in code.

Some ORMs offer a way to abstract DB interactions, and it’s a common solution to this problem to use these ORMs. EntityFramework offers such a solution that allows for multiple 3rd-party providers to implement specific SQL dialects – or potentially even NoSQL implementation – wrapping the same CRL metadata. It even has an in-memory provider that allows for local “mocked” operations that don’t need a live database. So, PasswordValidationService could be refactored to use EntityFramework’s DbContext instead of direct SQL queries:

public class PasswordValidationService : IPasswordValidationService
{
    private readonly IPasswordHashService _passwordHashService;
    private MyDbContext _myDbContext;

    public PasswordValidationService(
        IPasswordHashService passwordHashService,
        MyDbContext myDbContext)
    {
        _passwordHashService = passwordHashService;
        _myDbContext = myDbContext;
    }

    public async Task<bool> VerifyUserPassword(string username, string password)
    {
        var userPasswordInfo = await _myDbContext.Users
            .Where(user => user.Username = username)
            .Select(user => new { user.PasswordHash, user.PasswordSalt })
            .FirstOrDefaultAsync();
        
        return userPasswordInfo != null
            && Enumerable.SequenceEqual(
                userPasswordInfo.PasswordHash,
                _passwordHashService 
                    .HashPassword(password, userPasswordInfo.PasswordSalt));
    }
}

This is a much cleaner implementation. There is no SQL in the validation service. The exact database provider used by the DbContext is configured outside of this class, meaning it can change without this class changing.

However, there’s still Vendor Lock-In. Specifically, this implementation ties the business logic to EntityFramework. At some point in the future, the development team may determine that EF isn’t the correct solution and want to use a different ORM such as NHibernate or Dapper. Or they may want to switch to a NoSQL database that provides its own driver such as Couchbase or MongoDB.

Instead, consider this implementation:

public class UserPasswordInfo
{
    public byte[] PasswordSalt { get; set; }
    public byte[] PasswordHash { get; set; }
}

public interface IUserPasswordInfoLookup
{
    Task<UserPasswordInfo> GetUserPasswordInfoByUsername(string username);
}

public class PasswordValidationService : IPasswordValidationService
{
    private readonly IPasswordHashService _passwordHashService;
    private readonly IUserPasswordInfoLookup _userPasswordInfoLookup;

    public PasswordValidationService(
        IPasswordHashService passwordHashService,
        IUserPasswordInfoLookup userPasswordInfoLookup)
    {
        _passwordHashService = passwordHashService;
        _userPasswordInfoLookup = userPasswordInfoLookup;
    }

    public async Task<bool> VerifyUserPassword(string username, string password)
    {
        UserPasswordInfo userPasswordInfo = await _userPasswordInfoLookup 
            .GetUserPasswordInfoByUsername(username);
        
        return userPasswordInfo != null
            && Enumerable.SequenceEqual(
                userPasswordInfo.PasswordHash,
                _passwordHashService 
                    .HashPassword(password, userPasswordInfo.PasswordSalt));
    }
}

In the previous examples, external tools were injected as dependencies into the business logic layer. That is, the dependency is defined somewhere else, and consumed here.

In this last example, however, the business logic layer is defining its own dependency: IUserPasswordInfoLookup, a service that knows how to lookup user password info based on a given username. The specific implementation of that lookup – whether SQL, MongoDB, Redis Cache, or flat file; using EntityFramework, Dapper, or direct queries – is irrelevant to the business logic layer.

The business logic layer is both consuming and defining its own dependency. At this point, the dependency is fully inverted. The business logic can be implemented and tested completely independently of the actual lookup mechanism. And the actual lookup implementation can be swapped out without having to change the business logic layer.

This last part of is of special interest. The specific implementation of IUserPasswordInfoLookup can exist in the same library, or it can be defined in an external library and injected via an IoC container. This is the basis of Hexagonal Architecture, which is an software architecture design pattern that enforces strict limits on business logic dependencies. All dependencies should be inverted, defined as “ports”, and implemented as “adapters” in external components that are plugged in at runtime.

By “inverting dependencies”, business logic can be implemented in complete isolation from all 3rd party tools. First and foremost, this prevents Vendor Lock-In. Even further, business logic can be implemented sooner in the development process, possibly even before those 3rd party tools are chosen.

This differs greatly from having a predefined set of tools foisted upon the development team at the start of work. Such tools might have to be shoehorned into to place to work with actual user requirements, or they might interact poorly with necessary domain design choices and fail required SLAs.

For example, if a cloud application has a need to use messaging, the development team could create a messaging infrastructure abstraction layer. The development team could perform basic integration testing with a simple AMQP client implementation and a RabbitMQ Docker container. Based on observations of the integration test performance, the team could decide when – and if – to start using a cloud-native solution for the target cloud platform such as Amazon MQ, Azure Service Bus, or just stick to using a self-managed RabbitMQ container in production. This allows for full analysis and understanding of the costs associated with the final messaging solution to be made in-parallel with or after the business logic implementation.

The Dependency Inversion Principle isn’t just an object-oriented software design principle. It’s the foundation for a robust architectural design pattern that can greatly improve quality-of-life not only for the developers writing the software, but testers, and end-users as well. Of the 5 SOLID principles, this last one has the most impact and can be among the easiest to master.