Practical Principles for Engineering Teams
Throughout my software engineering career, I’ve had the good fortune to be part of some great teams from which I’ve learnt loads. It’s been great to see what characteristics work and don’t work, what’s hard and what’s easy.
The key feature of high-productivity teams that I’ve seen is a good push-and-pull dynamic between pragmatism and idealism. Full-throttle pragmatism results in a continual process of dirty, tactical hacks that slows any tech organisation right down eventually. On the other hand, over-idealistic teams who strive for The Perfect Code won’t deliver much value and will just exist to serve themselves.
So, here’s a few tips that I believe will help lay the foundations for keeping engineering teams productive.
Be business builders, not coders #
Engineers receive many titles (several being self-appointed): “Techies”, “Code monkeys”, “Devs”, “Nerds”. It’s an identity most of us are used to and one some of us even quite like. But the issue is that it can easily put engineers in a box, that their responsibility is solely to stay in their lane. Write code and let the business folk do the business work.
It’s a setup that many embrace, for many reasons. A love of programming and a disinterest in how companies work can make the prospect of coding, and just coding, all day long very attractive.
But if you’re just writing code for the sake of it, then the opportunity for adding value is slim. You may feel productive, but may not be producing value. You may be writing loads of tests, getting your infrastructure just right, and yet not be productive.
Many of us forget the reason employers hire us isn’t to write code, but to grow their business using technology. To do that, you have to understand how your employer makes money, what costs are incurred, what’s important to users, etc. Your priority is to build businesses, not your codebase, so take a look at the challenges you face as a company and use your technical skillset to address them.
Build strong team values #
“Teams are greater than the sum of their parts”
A team which is built to collaborate closely, support each other and level-up it’s members is much more productive than were those individuals acting in isolation. But good teams aren’t created by simply sitting people next to each other and letting the magic happen, it requires careful nurturing.
Build the right culture #
Building teams and fostering culture is tricky. To make a team, you need to win over members to the idea that everyone’s there to help them and vice versa. Instilling the right values and attitudes to get that buy-in is the first step.
- Trust — Building trust and cohesion between team members is key. If someone on your team was unreliable and never upheld their responsibilities, then you likely wouldn’t trust them. You’d be less inclined to collaborate with them and wouldn’t put as much weight in their opinions. If you sacrificed time to help them out, but they were consistently unwilling to reciprocate, then trust is damaged and the team suffers as a result.
- Empathy — Being human is something everyone has to deal with (for better or worse!). When things go wrong and people need help, it’s important to empathise and support them where you can.
- Positivity — Make enthusiasm, praise and gratitude cornerstones of your culture. Perpetual negativity in a team has the power to kill it, it’s members sent scrambling for a more pleasant environment, in another team or even another company.
Succeed and fail as a team #
When teams win, they should win together. Rewarding specific people, despite efforts from everyone in the team can cause tensions and rifts.
Exactly the same can be said for when teams screw up, attributing blame and punishing specific people for what was a team failure also creates disharmony. But is that fair for team-members who were only tangentially involved in a failed project? Who did their part flawlessly and were let down by the actions of others?
A team should act as one, and if it isn’t, it’s failing. It’s everyone’s responsibility to steer a project back on course should they see it going off a cliff. To act as one, it’s important to allow input from all members on all aspects of a feature and to action the input where appropriate. Fail to do this and no will bother giving input and taking responsibility because they know they’re going to be ignored. Everyone’s focus will return to their sole responsibilities and collaboration will suffer.
When projects become team responsibilities, the dialogue shifts from“I”-centric to“we”-centric. It goes from“What do I need to do?” to“what do we need to do?”. It builds collaboration, camaraderie and the need to support everyone on the team, particularly those who may be struggling.
Always be sharing knowledge #
When teams get to a certain size, it’s hard for everyone to know everything, information stops flowing“by osmosis”. Besides, knowing everything isn’t usually necessary or desirable, knowing enough is the target. When teams don’t know enough mistakes start to happen and effort starts being wasted.
For engineering teams, I believe there’s several useful practices that when adopted can go a long way to keeping teams well-informed:
- Standups — Short daily catch-ups (10−15 minutes max) are a good way to share high-level updates with team members. If someone decides a particular update is directly relevant to something they’re working on and they require more detail, they can have a more indepth conversation outside of the standup.
- Pair-programming — Pair programming is a fairly controversial practice whereby two developers work on a single task together on the same machine. It’s other merits and faults aside, it’s a great tool for sharing codebase knowledge across engineers. When employed on critical features it really shines. It guarantees at least two engineers have indepth knowledge of how a feature functions, so should one author be off-sick and suddenly a production issue erupts, there’s another on hand to resolve it.
- Code reviews — These are great opportunities for engineers to get an eyes-on look at code that their peers are writing. Should in the future, they be required to update it, they’re already familiar with it. It’s also a prime opportunity for junior engineers and new-joiners to get familiar with team development practices. Code-reviews aren’t simply a gatekeeping practice, they’re an effective method to share knowledge around teams.
Make it easy to do the right thing #
This is one of my favourite principles.
Expanded on a little, it means that in the code we write, the systems we architect, the channels of communication we build, it should be obvious and require less effort for people with limited context to behave in a way that’s desirable. Often team members go off-piste and start causing chaos because they didn’t properly understand the rules that had been agreed by others beforehand.
Take MVC application frameworks, for example. By establishing conventions and being consistent, it’s easy for a new engineer who has limited knowledge of a Ruby on Rails codebase, but experience with MVC, to start writing Rails code quickly because they know the rules of the game. In other words, using a commonly-understood architecture makes it easy for the engineer to do the right thing. It’s easy to write code in the right way, in the right place.
Doing things this way avoids, in a very natural way, the need for documentation and active education. People just get it and get on with their job.
Adopt Continuous Delivery #
Getting your team setup with a Continuous Delivery (CD) pipeline is an important tool in maintaining productivity for several reasons.
High throughput, high momentum #
Momentum is about capitalising on the enthusiasm and buzz generated by delivering value and channeling it into delivering more value.
A good delivery pipeline is one that enables a team to keep up momentum by facilitating multiple releases every day. When release throughput is monthly, fortnightly or even weekly, momentum can quickly die. We settle in, we say“we’ve not shipped anything for two weeks, what’s a week more?”. RIP productivity.💀
There are some valid challenges to keeping up momentum. For example, slow manual processes imposed by Apple and Google creates problems for the“release often” mantra for mobile app engineers. Despite these barriers, every effort should be taken to ship as often as is feasible within the imposed constraints.
Release early #
By committing to shipping early, it forces us to think about features in their simplest forms so that we can deliver value as soon as possible. It make us answer important questions early on, such as:
- “What hoops need jumping through before this feature can be shipped?”
- “What’s the minimum level of functionality we’re comfortable shipping?” Front-loading these questions avoids them being asked at the last-minute, causing preventable delays.
Release often #
By shipping often, it forces us into chunking features into multiple small releases, which reduces the complexity and risk of each one.
It reduces service outages caused by bad releases and when production issues do arise, they are easier to diagnose and resolve because of the smaller surface area involved in the release.
Lower release complexity reduces the likelihood of difficult rollbacks that can damage team momentum and cause features to languish longer than they should.
Pipeline dashboard #
Talking more tactically for a moment, having a dashboard visible to engineers which visualises the current state of your deployment pipeline is another perfect example of making it easy to do the right thing.
- It gives every engineer an up-to-date perspective on what is currently being shipped, so they can make better decisions on how to schedule their release and who they need to coordinate with. Less time is wasted on communication.
- It gives a clear picture of the current state of each environment (i.e. staging, production). Everyone is clear about what code is where.
- Should an issue arise while an update is making its way through the pipeline (e.g. a test fails), it’s soon apparent. The responsible engineer can quickly address it and keep things flowing.
Test (proportionally) #
When in product-based engineering teams, automated testing is unquestionably a good idea. It requires up-front effort, but it’s my belief that long-term, it speeds development up by identifying regressions straight away. By integrating into delivery pipelines, they improve service reliability by blocking bad releases from reaching production. For a sizable product, not having a basic test suite which runs through critical user paths is a dangerous and frightening prospect.
But taken too far in the opposite direction is equally as dangerous. Over-testing can waste development time on niche edge-cases and cause headaches in the future when minor refactors and features cause hundreds of tests to fail. Days can be sunk into correcting overly-cautious tests.
A judgement call needs to be made when deciding how much to test. The key things to consider are:
- Importance — Is this feature a critical flow? If it were to break, how many users will be affected and what will be the impact to the business? Could important data be lost forever?
- Difficulty — How hard is this feature to test? Does the effort to write tests outweigh the damage if it breaks?
Descope, descope, descope #
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” — Antoine de Saint Exupéry.
The perfect feature is one which delivers maximum value with minimum effort. Therefore, given a scope of work, if we can reduce the functionality required to deliver the desired value then we’re saving time and effort. We’re being far more productive.
If your team takes a long time to deliver giant features and is missing deadlines, the solution isn’t to work long hours and grind yourselves down. Try descoping your spec down to the absolute minimum required to achieve your targets. This may require compromises, but to be pragmatic is to be comfortable with compromises. Be careful not to throw the baby out with the bathwater though, always keep one eye on the goal lest you compromise away all the value you set out to deliver.
Scope creep rabbit holes #
Often engineers find themselves down scope creep rabbit holes. A relatively simple feature turns ugly because the engineer decides this is required and that isn’t as it should be, etc, etc. Before you know it, this simple feature has grown to twice the size and taken three times as long as it should have. I’ve done it plenty of times, most of us have.
The tricky thing is lifting yourself out of the hole once you’re down it, which is hard. Often the cruel sunk cost fallacy pushes us into digging deeper and deeper. We need to recognise when it’s happening by asking ourselves“is this absolutely required?”. Correct your path to one that delivers the value you set out to deliver and nothing more. Descope, descope, descope!
Find a balance with technical debt #
When we make compromises with code, we incur technical debt that needs to be paid off at a later date. I love the“debt” analogy because it work so well.
People take out debt so they can make a big purchase (e.g. a car you need for your new job). They then gradually, over time pay back that debt, with a bit of interest. If you’re not making those payments, the interest is going to spiral and you’re going to be in a state where every penny you earn is going towards paying off that debt.
The exact same is true for technical debt. You make a bodgey hack that goes against the established architecture to quickly ship a big feature. Taking on the debt may have been the right call in the circumstances, getting the feature shipped on time was the priority. Six months later, the bodgey hack is still there, only now it’s been used as a template for similar features by another engineer! Things can’t go on like this much longer, the time has finally come for the debt to be paid, but now with interest. Half the team downs tools to unpick the bodgey hack.
This scenario aside, never incurring technical debt can be just as bad. Over-polishing things and making every feature perfect, regardless of importance, is a total waste of time. Spending hours and hours polishing an experimental prototype that is likely going to be binned isn’t worth the effort.
We just have to be sure that when we accumulate technical debt, we make a record of it and eventually prioritise a fix. Not doing so will grind productivity to a halt. Set up a process to periodically sort the things that need addressing most urgently, or better yet, fix them in future features that touch code containing the technical debt.
- Be business builders, not coders
- Build strong team values
- Make it easy to do the right thing
- Adopt Continuous Delivery
- Test (proportionally)
- Descope, descope, descope
- Find a balance with technical debt
Hi, I'm Joe Forshaw.
I'm a Freelance Software Developer based in Manchester, UK.
For news about my posts and things I'm working on follow me on Twitter.