Building a Team Around Pair Programming - Growing Into Agile

In this blog post, I’ll be describing a set of not-so-common practices we’ve employed in a small team and the benefits we’ve gained. I hope to encourage more people to give some of these practices a try, adapting them to their use case. No approach is one-size-fits-all, so pragmatism is the key here.

A bit on Agile mindset

I don’t know about you, but I’ve always considered “Agile” to be a dirty word. It’s often an excuse for a lack of organization, unclear requirements, and a stressful work environment. I’ve met senior developers claiming Agile to be a con, designed to squeeze the developers dry. And I had no reason to doubt that stance. At the same time, some tenets of the Agile movement are reasonable and obviously a good idea, which I’ve come to realize through time.

Quick and reliable feedback is the first that comes to mind. How do you know that what you’re building is what your clients and users need without timely feedback? You could have requirements communicated perfectly (which is not likely in practice), but when the clients get the finished product, they can often tell that’s not what they need. That’s what they specified, true, but they know better now. The reason for that is the fact that software is extremely complex (by understanding that fact alone, I’d say you gain a great insight into many of the problems of our industry). Each piece of a software system is unique, with a unique interaction with other parts of the system and the ever-changing external world.

With the complexity of software and the impossibility of perfect requirements in mind, it becomes clear that perfection in software is an unreachable goal and that one should be wary of striving for it. That brings us to the iterative approach. We can’t expect to get the whole system right the first time, so we add functionalities incrementally, getting timely feedback through the process.

As I said, I’ve come to realize over time these principles make sense, but it took me one more step to fully grow into the Agile way of thinking. That was the opportunity for me to lead a small team (5 people in total, 2-3 full-time).

Team lead role

For this project, I was determined to build a tightly integrated team, which can deliver functionality quickly while maintaining a high level of quality. My role, as I saw it, was to point the team in the right direction, while avoiding forcing anyone to do things they disagreed with. We’ve come up with an approach that could perhaps be summed up with the following 4 points.

Note: I won’t be trying to define these practices or list all the benefits, but rather give a short summary of why I find these important and describe our experience.

1. mob/pair programming

Did anyone ever see pull request reviews done 100% right (or at least close to it)? If so, do let me know! Usually, as soon as people create a PR, they consider the task done. Other people are often hesitant to comment asking for changes (you don’t want to have the person comment on your PR, do you!?) since that’s forcing the person to return to the task they’ve considered done. No matter how much you appreciate the feedback you get, going back to the previous task will sometimes be frustrating. Also, you don’t want to prolong the PR review process even more, so you just implement the requested changes, whether you agree with them or not. And don’t even get me started on the fact this is all blocking releasing the functionality (and getting feedback), with the code just sitting there in the feature branch, while code conflicts are accumulating.

What’s the alternative? It’s the immediate code review process, through pair or mob programming. Ever since trying pair programming for the first time, I’ve considered it the best way to share knowledge and bond the team members. Besides the immediate code review and the best possible knowledge sharing, pair programming boosts your productivity immensely. The reason for that is the fact there is always someone to unblock you when you get stuck and suggest solutions without you having to Google them. If you’ve ever tried pair programming, you’ll know what I’m talking about - it’s exhausting, but the productivity level is unmatched. Pair programming is especially beneficial at the beginning of the project since the team can agree on and remember different style and quality guidelines.

For the first month or so, we’ve done mob programming pretty much the whole time. Our working hours didn’t have to overlap 100% of the time, but if multiple people were working at the same time, they’d get on a call and work together. If there was no one else online, you’d work alone.

This approach has served us extremely well in team bonding and knowledge sharing. It was exhausting at times, but if you take enough breaks and give yourself enough flexibility, it’s really possible to do full-time pair programming. And the productivity will go through the roof!

2. Small commits

Doing small commits has many important benefits. It means your change gets merged faster, which means faster feedback. Also, it reduces the chance of merge conflicts, since you’re merging the code more often and in smaller batches. It makes the change easier to test and understand. It also boosts your motivation since you’re not stuck implementing a feature for 5 days without pushing any code (and explaining yourself on the daily stand-up every morning) but, instead, you push something every day.

How small? We usually went for the smallest possible beneficial increment, which can be a one-liner but can also be dozens of files if changing some widespread functionality. What matters is that the commit does one thing, even if it spans many files.

3. TDD

Test-driven development has gained some bad rep due to some people taking it to the extreme, but I still consider it essential for good software testing.

I don’t think anyone enjoys writing tests after they’re done with the functionality. When you have users and other developers waiting for your change, implementing tests at the end can soon start feeling like a burden. This automatically means that you’ll soon start skipping on adding tests. And if you’re, at the same time, ignoring the previous point (small commits) and doing large changes (without tests), then may God help you and your teammates. Code without tests is legacy code (as per the definition given by Michael Feathers in “Working Effectively with Legacy Code”, which I agree with) since developers will be afraid to change it, which means it’ll rot with time.

On the other hand, writing tests beforehand forces you to think through the required change while also giving you high-quality tests. Sounds like a recipe for quality code to me!

Our approach? We didn’t obsess over the type of tests we wrote, keeping it as easy for us as possible. When implementing a new functionality from scratch, an end-to-end test was good enough for us. If implementing an edge case or a bug fix, we’ve used integration or unit tests. Unit tests were mostly used for cases not requiring much integration/mocking. We consider this the pragmatic approach and it has served us well so far.

4. Trunk-based development

If you adopt the 3 previous practices, making an additional step towards trunk-based development comes naturally.

When using feature branches, you have to deal with branches not being in sync, big merge conflicts, waiting for PR reviews, etc. With trunk-based development, all of these distractions are minimized and you can just focus on coding. When you’re done with your commit you pull, push (ideally, you have editor shortcuts for these) and you’re back to coding! You don’t lose your focus. Any eventual code conflicts will usually be minor since other people are doing small commits as well. The productivity boost you feel with this is amazing!

In our setup, we’ve had tests/linters/formatters run locally on commit and push. This would prevent you from pushing the code if the build is failing. However, we didn’t include e2e test suite run, since it would require the whole system to be running whenever you’re pushing a change, causing flaky tests. Instead, we had a central build system running after the code is pushed, which would prevent the release from being created and deployed in case of build issues. Still, this didn’t stop the code from being merged into the trunk. Developers were expected to run the e2e tests manually on their local machine before pushing, which is far from ideal, but I think it’s 100% worth the simplicity gained by not having to deal with branches and pull requests.

How’s that served us?

Saying these practices have served us well would be an understatement. Some of the benefits we’ve gained are:

  • Better team communication
  • Improved code quality
  • Shared code ownership
  • Greater feeling of motivation
  • Less stress
  • More frequent iteration

Experience working in this team was unlike anything I’ve experienced in my previous teams. The level of connection with other team members and the feeling of interrupted productivity made me enthusiastic for work every morning and a bit sad when it’s time to end the working day.

Conclusion

I’ve described 4 practices I consider great contributors to a healthy work environment. Being able to focus on one change at a time (small commits) makes it much easier to write tests beforehand (TDD). With these in place, trunk-based development opens up as a possibility, while pair programming really brings every step of the process to a whole new level.

Call up a team member for a pair programming session and give some of these a try!

comments