The siren call of wrong-way dependencies
Dependencies that point in the wrong direction are one of the easiest and most costly design mistakes one can make.
Dependencies that point in the wrong direction are one of the easiest and most costly design mistakes one can make. Easy because they’re often the path of least resistance. Costly because they make it harder to make changes.
What’s a wrong-way dependency? Higher level, more abstract components that depend on lower level, more concrete ones. Dependencies should be inwards pointing, in the sense that the more fundamental parts of your system sit on the inside and the implementation details on the outside [1].
Introducing such wrong-way dependencies is extremely easy and I see it done all the time, even by experienced engineers. They give you short-term gain for long-term pain.
Consider the following example: Suppose your system interfaces with a bunch of different third-party service providers which add different functionality to your product (say, different payment gateways for different payment methods). Further, suppose that your customers need to be registered in advance with each provider prior to being able to use the functionality offered by it. For any given customer, you need to be able to tell at runtime whether the functionality is available to them (e.g. they can use a given payment method). There are two ways to address this:
Add a boolean property to your customer model for each provider that indicates whether a customer has been registered with that provider, defaulting to false.
Add a way to query each service provider for whether a customer has been registered with it. The provider’s API might support this directly, or if not we’d wrap it and maintain a list of registered customers ourselves.
Option 1 is tempting because it involves less work at the outset. But now suppose we want to make any of the following changes:
Introduce a new service provider
Change the identifier or reference for a service provider
Add more fine-grained permissions
All of these now entail touching customer code or data, even though they have no bearing on the core logic for managing customers. Changes are now more costly and riskier. Option 1 was faster to implement but has increased the blast radius of changes significantly.
How do you avoid wrong-way dependencies? Think about what kinds of product changes you will need to make in the future, then avoid dependencies that make them difficult. Of course it’s hard to predict how requirements might change, but the big axes are usually fairly obvious. Avoid speculation and focus on the obvious ones.
[1] Bob Martin makes this point in many ways in Clean Architecture. “Depend in the direction of stability” is a particularly succinct one.