Behavioural Coupling is a Silent Killer — December 14, 2023

Behavioural Coupling is a Silent Killer

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.

Elegance — December 12, 2023

Elegance

You can argue almost anything with SOLID. In my experience, elegance is more important.

An elegant solution is immediately understandable by everybody. It’s a way of thinking about the problem that becomes _everybody’s_ way, as soon as they see it.

Questions about how we would handle x, y or z have obvious answers – they follow naturally because an elegant way of thinking gives us leverage. 

In general, we should judge our ideas less by how well they conform to some design pattern, and more by how well they bring understanding to our team. By how much leverage they give us to solve our problems, by their simplicity, and – sometimes – by their brevity. To me, that’s elegance.

Collaborate Your Meetings to Death —

Collaborate Your Meetings to Death

Most meetings are only necessary because of gaps in shared context. If we all did everything together, there would be no reason for meetings (except with external stakeholders). But teams can easily fall into a a vicious cycle. Inundated by meetings, people begin to isolate themselves, creating a vacuum in shared context that gets filled by more meetings. 

Break the cycle. Pick a meeting that’s being used to share context and find a way to share it earlier and more continuously.

Doing It Well — December 7, 2023

Doing It Well

If you care about doing it well, you are most of the way there.

That’s no small thing. Doing it well means caring about improvement more than ego. It means being challenged instead of comfortable, humble instead of prideful, open instead of defensive, earnest instead of cynical, and hard-working instead of lazy. It is believing the best way is still out there and going on a mission to find it every day.

If you can find it in yourself, you are very lucky. If you can practice it well enough that others join you, then you can build a great team – and a great team is something very special.

Delivery Today and Tomorrow — May 3, 2022

Delivery Today and Tomorrow

My way of thinking about technical debt is a bit different to what I usually hear 😅 Let’s start by rejecting the usual framing – the idea that we are in some eternal struggle to find the right balance between speed and quality.

We’re not balancing speed and quality.  We’re balancing delivery today against our ability to deliver tomorrow.  We care a lot about delivery today, which may tempt us to compromise on delivery tomorrow. But do you really think that tomorrow, we will care less about delivery? Has that been your experience so far? 

There are cases where delivery today is an existential necessity, but they are rare. Even startups need to deliver consistently on a long enough timeline for technical debt to catch them. But that doesn’t mean we should spend today developing complex architectures, implementing fancy design patterns or factoring new code exhaustively – there are good reasons to create technical debt.

Imagine you are on a trip and have just arrived at your hotel. Your luggage is full of useful items you may need during your stay. Do you assume you will use every item and unpack them all right away? Do you immediately fill cupboards and drawers with your belongings? Of course not! You start by keeping everything together so you know where it is (high cohesion) and unpacking things as you need them (YAGNI).

People give similar advice about building microservices. Don’t try to draw service boundaries at the start because you’ll get them wrong and it will be hard to fix. Keep things together in one service (high cohesion) and extract them only when boundaries become clear (YAGNI).

There are some life lessons here for us!

  • We are usually wrong at the start
  • Arranging is much easier than rearranging

That first point is important. When we start building something, we are probably wrong. Wrong about how our domain works, wrong about how to model it, wrong about which parts of the system will need to be extensible, wrong about our cross-functional requirements – there are so many things to be wrong about! The more heavily we factor our code, the more we pour concrete on our wrong assumptions. The more we arrange things at the start, the more difficult they will be to rearrange later when we find out we were wrong.  

This “we’re probably wrong at the start” thing can be a bit tough for experienced people to accept, as we may have built our professional ego around the idea that we are good at being right. If you think you can get things right the first time, this post isn’t for you.

But if you’re with me so far, then my advice is this. Apply what we’ve learned about luggage and microservices right down to the class and function level. Don’t heavily factor your code at the start – just do enough. To build enough, we can use outside-in, test-driven development, test-driven design and close collaboration to produce the simplest, smallest, least-factored thing that works. Anything we are tempted to do beyond that, we can consider technical debt.

Working this way, our initial solution will be:

  • As simple as possible, because we will produce it outside-in while practicing test-driven development
  • Well structured, because test-driven design will guide us in writing data-driven code, separating concerns and avoiding complex conditional logic
  • Using domain language that makes sense to our business, because we are working closely with non-engineers
  • Sensible to our team, because we are mobbing or pairing with rotation
  • A step in the right direction, because we have thought about the problem holistically (but a small step only, because we have not thought too hard or too long)

The result of all this will be some software and some technical debt, so when and how should we pay that debt off? A few years ago, I would have suggested we briefly record technical debt on a technical debt wall, with axes for effort and value. I’d have suggested reviewing the wall at every story kick-off and picking up items that will make delivery of the coming story easier. That’s not a bad approach – it’s better than what most people are doing.

Now I believe technical debt walls are a local optima. In a team working as we have discussed here, categorising and prioritising technical debt is of little value:

  • Our opinion about how to improve something will change as our skills and knowledge grow
  • Our opinion about what is worth improving will change as we are asked to adjust and extend our software in ways we did not initially predict
  • We may not work in that part of the codebase again in a relevant timeframe anyway

It is more sensible to ask – before making any change to our software – “Can we improve our engineering design *a bit* to make the coming change easier?” Why only “a bit”? Because we don’t want to engage in a wholesale, big-bang redesign based on the need for a single change. Every time we change our software, we want to improve its design in a small increment, so that the kind of change we are trying to make is easier next time. This way our design will gradually evolve to support the kinds of changes we are actually making, in the areas we are actually making them. We don’t need to gamble on what should be extensible, how extensible it should be and in what ways we will need to extend it.

Put everything together and the entire approach is super simple:

  1. Build the simplest thing we can
  2. Every time we change it, make the change easier with a small improvement to our engineering design

With this approach in mind, the dichotomy between delivery and technical debt is exposed as false. We improve our engineering design when we deliver something and when we deliver something, we improve our engineering design. There is no need to categorise work as ”technical debt” or “delivery”. Such categorisation and the careful management that usually follows is a useful local optima for teams that are not evolving their engineering design continuously enough yet, but it shouldn’t be our ultimate goal.

“Technical debt” as a model for thinking about the need to continuously improve our engineering design is a good raft – it can help you get across the river as you learn how to do the right amount of work up-front and master improving it gradually over time. But when you get to the other side of that river, you should put the raft down.

Pressure is a Resource — November 18, 2021

Pressure is a Resource

We talk a lot about the best ways to work, but we don’t talk very much about how to move our organisations toward them.

As a former consultant, I grappled with this every day. Even with a clear idea of some alternative way of working, a large team of people ready to teach it and good executive support, it is still very difficult. One reason for this is that we hastily squander a powerful resource – pressure. 

When a problem impacts our business, organisational pressure is created. We all want to relieve that pressure and that shared desire is a special thing – we can harness it to effect positive change. Unfortunately, many organisations react reflexively and take the path of least resistance. That is how pressure differences work, after all. We look for quick fixes like overworking “temporarily” or juggling more WIP. But quick fixes are not always good fixes.

What if we are only moving the impact somewhere less obvious? E.g. we have started a critical piece of work to placate stakeholders, but the resulting increase in WIP is causing inefficiencies across all work? What if we are only changing the timescale of the impact so that instead of being hurt a little every day, we get hurt a lot all at once? E.g. we are overworking somebody and it will blow up in our face when they quit.

Our quick fixes may actually leave us in a worse place than before. The root problem is still not solved and there is little pressure left to drive improvement. We wasted our chance. 

It’s important that we not reflexively relieve pressure. Pressure helps us grow and change if we channel it wisely. We may need to use some of that pressure to make the immediate impacts of our problems manageable, but to build a great and enduring business we must direct as much of that pressure as we can into lasting solutions to our deepest problems. So spend your pressure wisely and don’t rush relieve it entirely. To improve continuously we need pressure and with no pressure, we can easily get stuck.

If you want a fleshed-out example, you can read my article about adopting trunk-based development. It’s an approach that offers some benefit at each stage, while still leaving or creating enough pressure to drive us toward the next one. 

Focus on Technique —

Focus on Technique

If you want to be a better competitive swimmer, you don’t ask for feedback on your lap times and trophies – you focus on improving your technique and know that results will follow. 

Most software developers are preoccupied with the properties of good code, but knowing what it looks like is not the same as knowing how to write it. A good critic isn’t automatically a good creator. 

How does the length of your red-green-refactor cycle impact your results? Does working outside-in or inside-out change your solution? Do different pairing styles lead to different outcomes? Do you get simpler code by thinking through the problem in advance, or by being deliberately naive? Does writing failing tests instead of passing ones impact your implementation? Do your team members work through problems in different ways and if so, what are the advantages and disadvantages of their approaches?

If you can explain SOLID principles but you can’t answer these questions, forget about *what* for a second and start thinking about *how*. 

Pair with your teammates. Let them show you their way of thinking and working and the next day, ask to show them yours. Set aside time together to reflect on your experience and discuss what you want to do differently next time. Do experiments together and share the results with your team. 

Focus on technique.

How to Stop Branching — August 9, 2021

How to Stop Branching

I know several developers that were forced into trunk-based development without preparation and had a terrible experience.  I don’t want to see that happen again, so I’m offering up this post as a practical guide to help your team safely climb down from their branches and practice continuous integration.

If you’re brand new to thinking critically about branching, you can read Martin Fowler’s explanation of continuous integration. If you’re a little further in, I love this blog post by Dave Farley. If you like branching and want to keep doing it, this isn’t the post for you.

Branching is a local optimum – it works just well enough for many teams to tolerate. It requires a number of interdependent practices and when we change one by itself, the system is degraded and the team reverts. When we change all of them together, we throw everything into chaos and nobody wants to try again. It’s a Gordian Knot I’ve spent my whole career untangling.

It has a way of recursively reinforcing itself. By facilitating working independently with limited collaboration, it delays feedback and causes rework during the pull request process, which in turn makes us feel that pull requests – and by extension branching – must be very important. Andrew Cain once told me that recursion is the most powerful force in the universe. Instead of fighting it, we’re going to take steps that will improve our lives now while preparing us for trunk-based development in the future.

Build in quality

Nobody wants to integrate poor quality changes into an unsafe codebase. That’s one of the things people attached to branching are trying to avoid – they want to know that each change is safe.

We can help them achieve this by driving the adoption of quality-focused development practices like:

  • Test-driven development
  • Overlapping coverage across multiple layers of testing
  • Tests as documentation
  • Static analysis
  • Linting

These techniques are more effective at catching defects than pull requests and also provide earlier feedback, reducing rework. They will add a lot of value, even if you keep branching.

Work in small steps

Even people who really like pull requests usually agree that smaller pull requests are preferable, because they tend to be reviewed faster and more thoroughly. More frequent integration also reduces the chance and size of merge conflicts.

We can switch from feature branching to task branching and work in a series of small, safe-to-deploy steps with a pull request for each one. We’ll spend less time debugging, receive earlier feedback from our peers and enjoy faster and more thorough reviews.

Many people are used to tearing everything apart and putting it back together again on their branch. It may take some people a lot of effort to learn a more incremental approach and you will have to be patient and persistent during this phase.

As you improve, you can begin working exclusively in small, production-ready commits. Even the handful of commits made on a small task branch can be made using the red-green-refactor cycle to ensure that your software is working at regular intervals, with only small changes between each known-good state. This will radically reduce cognitive load and debugging effort.

Use feature toggles

One challenge of breaking our work into small task branches is that we must decouple deployment and release. After all, we want to integrate our code – at which point it must be deployable – without releasing our unfinished story.

We can use feature toggles to achieve this – just think of it as runtime branching. We integrate our code, but delay releasing it by disabling it with a feature toggle.

In addition to facilitating more frequent integration, feature toggles allow us to quickly disable problematic features in an emergency. With the right tooling, we can even use them to do pre-release testing in production. Think of all the time we’ve spent trying to maintain production-like environments and test data when we could have just tested in production all along!

Feature toggles can be complex to manage. Putting them in the right places, tracking when they can be turned on and remembering to remove them may take some new tooling, some practice and even some workflow changes. Don’t rush this step – we need to be great at this before we can practice trunk-based development.

Collaborate more

Pull requests are now more frequent, but without increased collaboration this is usually slow and annoying. A little tension helps drive us to continue improving, but we have to guide our team carefully in this phase so they don’t learn the wrong lessons and go backwards.

Substantive pull request discussions cause unnecessary rework and can be taken as a sign that we did not collaborate well. We can eliminate this by working together throughout the development process using techniques like:

  • Pair programming with rotation
  • Thorough kick-offs
  • Tech huddles

We’ll get more context-aware, earlier feedback from more people. Over time, pull requests will become trivial – your team will be aligned before the pull request, reducing delay and rework.

Now, we can point out that the feedback achieved through continuous collaboration appears to be more useful than the feedback received in pull requests. If we were comfortable merging solo work with 2 pull request reviewers, shouldn’t we be even more comfortable merging paired work with a single pull request reviewer? What if we rotated pairs – isn’t the context-aware feedback of 3 close collaborators worth more than 2 peers with minimal context looking at a diff?

Stop branching

By now, we’ve already:

  • Built quality into our practices and codebase so that we feel confident integrating our work
  • Learned how to integrate more frequently by working in small steps and using feature toggles
  • Discovered that collaboration is more effective than critique and broken our dependence on pull request reviews for feedback

We’re ready. All that’s left is to be brave and ask our team if branches and pull requests are still adding enough value to justify the delayed integration and context switching they cause.

Collaborating Continuously — July 28, 2021

Collaborating Continuously

Some engineers have found close collaboration (by pairing, for example) quite challenging and feel that they are more productive when working on a branch by themselves. In a team setting, this impression of higher productivity is mostly illusory.

Ultimately, the people on your team need to understand, approve of, maintain and extend your work. If you don’t come to some understanding together during development, you will have to work it out at the end in a slower process with less shared context and more rework.

Most challenges experienced in close collaboration are temporary. With practice, pairing skills will improve, technical knowledge will be shared and ways of thinking and working will be exchanged. Your team will converge on effective patterns and practices, greatly simplifying collaboration.

So take that tricky pairing session in stride. Your team will get better at it and it’ll pay off.

Deadlines — March 13, 2021

Deadlines

Recently, one of the engineers on my team asked for a deadline. I refused to set one, then the CTO jumped in and also refused. It made me very happy and I bet it doesn’t happen at many companies, so I want to share why we did that in the hope that it convinces you to stop setting deadlines too.

My Perfect Team

Let me tell you about my perfect team – I’ve been on it several times.

Developers, a business analyst, a product owner, a QA and a designer sit together around a big table with no walls, dividers or big monitors in between them. We’re just a few people with our laptops, collaborating closely every day and pairing with each other across all roles.

We know how to iterate and have scoped out the earliest usable version of our product and the right level of fidelity.

We’ve written great stories according to INVEST principles and prioritised them very carefully so that we’re working on the most important things first. 

We’re united by our shared commitment to a common purpose. We care about what we’re doing and work at our best sustainable speed – challenging ourselves but not getting stressed.

Our team is vertical and autonomous – we have all the skills and authority we need to achieve our goals.

Setting a Deadline

Now that you know our team, I want to ask you a question. What will you gain by giving us a deadline?

Will we go faster? We’re already going at our best sustainable speed. Maybe people could work longer hours, but we only have so much mental energy to use and our work is intellectually demanding. Even if we do manage to get more done for a while, it will increase turnover and that will cost more than its worth.

Will you actually have any more certainty about when our work will be finished? You’ll have a date, but in your experience, have those dates really been reliable?

So we trade some possible short-term speed for increased turnover and we get a magic date that – if we are honest about our experience – we know we should not trust. And what will it cost?

Well, you’ll almost certainly guarantee that the work will not be delivered before the deadline.

For the deadline to be credible, the team will have to conduct regular agile estimation and velocity measurement. That will take a lot of time, delaying delivery.

The external pressure will impact the team’s sense of autonomy, further damaging morale and increasing turnover.

Finally, external knowledge of the deadline will create a false impression of certainty that encourages timeline based programme management. We’ll end up with a fragile, interdependent series of timelines that falls apart as soon as the first deadline is missed.

Move On

I know it’s hard to let go of deadlines, but we have to move on. Accept uncertainty. Stop being obsessed with what we can’t control and focus on what we can.

  • Replace talking about time with talking about scope and fidelity
  • Replace velocity measurement and estimation with better story writing and story slicing
  • Replace timeline setting with prioritisation
  • Replace external deadline pressure with shared commitment to a common purpose
  • Replace timeline-based programme management with creativity and concurrency