People worry a lot about static coupling, but not nearly enough about behavioural coupling.
Coupling in your code is easy to see and even measure. You can tolerate quite a lot of it, as modern IDEs are so good at helping us understand what’s going on.
Conversely, behavioural coupling is a silent killer. People usually introduce it with good intentions, not realising the damage they’re doing.
Good intentions go astray because this particular misstep often feels like a eureka moment. In a flash of insight, you realise that because some external system – which could be another unit of code or a distant microservice – works a _certain way_, you can solve some problem very easily in your system by _just doing x_.
Of course, this kind of thing makes us feel smart. We are using holistic knowledge of our software (and problematically, other people’s software) to save effort – which feels like efficiency – and produce briefer solutions, which feels like elegance.
But if our solution was formulated based on knowledge of how external systems behave, it will intrinsically depend on those behaviours and therefore be coupled to them.
Worse still, this coupling will be invisible. You won’t even know where to look for it. It might be in a simple function call, or hiding in complex patterns of message publication and consumption spanning major organisational boundaries.
Such solutions sit around like landmines. Every change in behaviour starts to feel risky, because you don’t know where it’s safe to step.
I’ve figured out how I have been avoiding this, but maybe it will sound a bit weird.
When I’m writing a unit of code, I’m _inhabiting_ it. I think and talk with my pair about what I – as the unit of code – am able to know through properly defined interfaces. I’m not allowing things outside of that to interfere with my behaviour (the behaviour of the unit), because I (as the unit of code) just don’t know about that.
For example, if my pair wants to assume a string passed to some public method will never be empty (say, because the current consumer is getting the value from validated user input with a minimum length), I put my cursor inside the method and say “Well, we (as the method) don’t know that right? We just know it’s a string.”
It’s important that our solution not rely on information that is known to the programmer _but not to the program_, because it could lead to – in this case – an implementation that explodes when an empty string is provided, which of course could happen at any time through a new consumer or a change to the existing consumer.
These things we feel clever about knowing and leveraging aren’t safe to build software on. They are and should be shifting ground, because behaviour should be easy to change.
Maybe this practice sounds a bit silly, but I’ve been doing it automatically for years. I think it’s helped me see the difference between what I believe is the case and what is programmatically guaranteed to be the case, which are very different things that can feel similar if you aren’t careful.
